Sign-In with Canton (SIWC) is the equivalent of Sign-In with Ethereum. The backend issues a structured message with a nonce, the user signs it with their wallet, and the backend issues a JWT scoped to the user's party. You attach the JWT on subsequent calls.
SIWC is not a transaction signature. It does not authorise on-chain state changes — those still go through prepare/execute with a separate signature.
The flow
// 1. Backend issues a challenge
const { message, nonce } = await fetch("/api/auth/nonce", {
method: "POST",
body: JSON.stringify({ partyId: party }),
headers: { "Content-Type": "application/json" },
}).then((r) => r.json());
// 2. Wallet signs the message
const { signature } = await provider.request({
method: "canton_signMessage",
params: { message },
});
// 3. Backend verifies and returns a JWT
const { token, expiresAt } = await fetch("/api/auth/verify", {
method: "POST",
body: JSON.stringify({ partyId: party, signature, nonce }),
headers: { "Content-Type": "application/json" },
}).then((r) => r.json());
// 4. Use the token
await fetch("/api/wallet/balance/" + party, {
headers: { Authorization: `Bearer ${token}` },
});The structured message
The challenge message follows EIP-4361 conventions, adapted for Canton.
Example:
arkena.io wants you to sign in with your Canton party:
alice::1220abc...
URI: https://arkena.io
Version: 1
Network: devnet
Nonce: 9b2d0c4f81a3...
Issued At: 2026-05-04T16:00:00Z
Expiration Time: 2026-05-04T16:10:00Z
Statement: I accept the Arkena Terms of Service.Fields:
- Domain (first line) — the host the user is signing in to. Lets the user verify they're signing for the right site.
- Party — the party ID this signature claims to be from.
- URI, Version, Network — boilerplate.
- Nonce — single-use, server-generated. Prevents replay.
- Issued At, Expiration Time — challenge lifetime, 10 minutes from issue.
- Statement — optional human-readable claim shown in the wallet prompt.
The wallet renders this as a sign-in card with the domain and party prominent. The user clicks once.
The JWT
The backend's response carries:
token— the JWT to attach asAuthorization: Bearer <token>.expiresAt— Unix timestamp.
The token is scoped to the single partyId it was issued for. Trying to
use it for another party returns 403 Forbidden. To inspect the party
attached to a token at any time, call GET /api/auth/me.
Rotation
When expiresAt is approaching (say, within 5 minutes), request a fresh
challenge and re-sign. Rotation is silent — the wallet doesn't show a
prompt for already-trusted origins on the same party.
To explicitly end a session, call POST /api/auth/logout.
What SIWC is not
- Not a transaction signature. SIWC signatures don't authorise on-ledger state changes.
- Not a permission grant. Connecting (
canton_connect) and signing in are separate. A user can be connected without ever signing in; signing in implies connection. - Not persistent across browsers. Each browser instance has its own JWT lifecycle.
What's next
- The signing model — how transaction signing differs from SIWC.
- Sign and submit transactions — using the JWT for prepare/execute calls.
- Error codes — what
403and signature-verification errors look like.