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

# Sign and verify messages

> Sign offchain messages and EIP-712 typed data with a modular wallet, and verify the signatures against the user's MSCA.

Modular wallets sign offchain messages for
[Sign-In With Ethereum (SIWE)](https://siwe.xyz/) authentication, EIP-712 typed
data for offchain orders, and other flows where your backend or a contract needs
to prove the user authorized the data. Plain messages and EIP-712 typed data are
two independent flows.

The examples below use the `smartAccount`, `client` (Web SDK), `bundlerClient`
(iOS, Android), and `modularTransport` (iOS, Android) returned by
[Create a modular wallet](/wallets/modular/create-a-modular-wallet).

## Sign and verify a plain message

Use `signMessage` for plain strings, such as SIWE messages or human-readable
consent strings. The Web SDK verifies through viem's `verifyMessage`. iOS and
Android verify by calling `isValidSignature` on the smart account contract
directly.

<CodeGroup>
  ```typescript Web SDK (TypeScript) theme={null}
  const message = "Sign in to MyApp at 2026-06-24T20:00:00Z";

  const signature = await smartAccount.signMessage({ message });

  const isValid = await client.verifyMessage({
    address: smartAccount.address,
    message,
    signature,
  });
  ```

  ```swift iOS SDK (Swift) theme={null}
  Task {
      do {
          let message = "Sign in to MyApp at 2026-06-24T20:00:00Z"

          let signature = smartAccount.signMessage(message)

          // Smart account signatures verify via EIP-1271. The contract
          // returns EIP1271_VALID_SIGNATURE (0x1626ba7e) when valid.
          let digest = Data(message.utf8).sha3(.keccak256)
          let function = ABI.Element.Function(
              name: "isValidSignature",
              inputs: [
                  ABI.Element.InOut(name: "digest", type: .bytes(length: 32)),
                  ABI.Element.InOut(name: "signature", type: .dynamicBytes),
              ],
              outputs: [ABI.Element.InOut(name: "magicValue", type: .bytes(length: 4))],
              constant: false,
              payable: false
          )
          guard let data = function.encodeParameters([digest, signature] as [Any]) else {
              return
          }

          guard let toAddress = EthereumAddress(smartAccount.address) else { return }
          let transaction = CodableTransaction(to: toAddress, data: data)

          guard let callResult = try? await Utils().ethCall(
              transport: modularTransport,
              transaction: transaction
          ),
          let callResultData = hexToData(hex: callResult),
          let decoded = try? function.decodeReturnData(callResultData),
          let magicValue = decoded["0"] as? Data else {
              return
          }

          let isValid = (EIP1271_VALID_SIGNATURE == magicValue.bytes)
      } catch {
          print(error)
      }
  }
  ```

  ```kotlin Android SDK (Kotlin) theme={null}
  CoroutineScope(Dispatchers.IO).launch {
      try {
          val message = "Sign in to MyApp at 2026-06-24T20:00:00Z"

          val signature = smartAccount.signMessage(context, message)

          // Smart account signatures verify via EIP-1271. The contract
          // returns EIP1271_VALID_SIGNATURE (0x1626ba7e) when valid.
          val digest = toSha3Bytes(hashMessage(message.toByteArray()))
          val function = Function(
              "isValidSignature",
              listOf<Type<*>>(
                  Bytes32(digest),
                  DynamicBytes(Numeric.hexStringToByteArray(signature)),
              ),
              listOf<TypeReference<*>>(object : TypeReference<Bytes4>() {}),
          )
          val data = FunctionEncoder.encode(function)
          val resp = bundlerClient.call(
              smartAccount.getAddress(),
              CIRCLE_WEIGHTED_WEB_AUTHN_MULTISIG_PLUGIN.address,
              data,
          )
          val decoded = FunctionReturnDecoder.decode(resp, function.outputParameters)
          val isValid = EIP1271_VALID_SIGNATURE.contentEquals(decoded[0].value as ByteArray)
      } catch (e: Exception) {
          e.printStackTrace()
      }
  }
  ```

  ```java Android SDK (Java) theme={null}
  try {
      String message = "Sign in to MyApp at 2026-06-24T20:00:00Z";

      CompletableFuture<String> signatureFuture = new CompletableFuture<>();
      smartAccount.signMessage(context, message, new CustomContinuation<>(signatureFuture));
      String signature = signatureFuture.join();

      // Smart account signatures verify via EIP-1271. The contract returns
      // EIP1271_VALID_SIGNATURE (0x1626ba7e) when valid.
      byte[] digest = toSha3Bytes(hashMessage(message.getBytes(StandardCharsets.UTF_8)));
      Function function = new Function(
          "isValidSignature",
          Arrays.asList(
              new Bytes32(digest),
              new DynamicBytes(Numeric.hexStringToByteArray(signature))
          ),
          Arrays.asList(new TypeReference<Bytes4>() {})
      );
      String data = FunctionEncoder.encode(function);

      CompletableFuture<String> callFuture = new CompletableFuture<>();
      bundlerClient.call(
          smartAccount.getAddress(),
          CIRCLE_WEIGHTED_WEB_AUTHN_MULTISIG_PLUGIN.INSTANCE.getAddress(),
          data,
          new CustomContinuation<>(callFuture)
      );
      String resp = callFuture.join();

      List<Type> decoded = FunctionReturnDecoder.decode(resp, function.getOutputParameters());
      boolean isValid = Arrays.equals(
          MscaConstantsKt.getEIP1271_VALID_SIGNATURE(),
          ((Bytes4) decoded.get(0)).getValue()
      );
  } catch (Exception e) {
      e.printStackTrace();
  }
  ```
</CodeGroup>

## Sign and verify EIP-712 typed data

Use `signTypedData` for structured data. Define the `domain`, `types`, and
`message`, and the SDK produces an EIP-712 signature your contracts and backend
can verify.

<CodeGroup>
  ```typescript Web SDK (TypeScript) theme={null}
  const typedData = {
    domain: {
      name: "MyApp",
      version: "1",
      chainId: 421614,
      verifyingContract: "0x...",
    },
    types: {
      Order: [
        { name: "maker", type: "address" },
        { name: "amount", type: "uint256" },
        { name: "expiry", type: "uint256" },
      ],
    },
    primaryType: "Order",
    message: {
      maker: smartAccount.address,
      amount: 1_000_000n,
      expiry: BigInt(Math.floor(Date.now() / 1000) + 3600),
    },
  } as const;

  const signature = await smartAccount.signTypedData(typedData);

  const isValid = await client.verifyTypedData({
    address: smartAccount.address,
    signature,
    ...typedData,
  });
  ```

  ```swift iOS SDK (Swift) theme={null}
  Task {
      do {
          let signature = try await smartAccount.signTypedData(typedData: typedData)
          // Verify by calling isValidSignature on the smart account contract,
          // using the EIP-712 digest of the typed data as the digest input.
      } catch {
          print(error)
      }
  }
  ```

  ```kotlin Android SDK (Kotlin) theme={null}
  CoroutineScope(Dispatchers.IO).launch {
      try {
          val signature = smartAccount.signTypedData(context, typedData)
          // Verify by calling isValidSignature on the smart account contract,
          // using the EIP-712 digest of the typed data as the digest input.
      } catch (e: Exception) {
          e.printStackTrace()
      }
  }
  ```

  ```java Android SDK (Java) theme={null}
  try {
      CompletableFuture<String> signatureFuture = new CompletableFuture<>();
      smartAccount.signTypedData(context, typedData, new CustomContinuation<>(signatureFuture));
      String signature = signatureFuture.join();
      // Verify by calling isValidSignature on the smart account contract,
      // using the EIP-712 digest of the typed data as the digest input.
  } catch (Exception e) {
      e.printStackTrace();
  }
  ```
</CodeGroup>
