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: mainnet
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 — the challenge message lifetime (10 minutes from issue). After expiry the user has to request a fresh challenge before signing.
- 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 JWT lives for 4 hours from issue. The backend silently rotates
it on any authenticated call older than 30 minutes (handing back a
fresh Set-Cookie so the cookie copy stays current), and accepts the
previous token for a 5-minute grace window after rotation. You almost
never need to manage the lifecycle manually — but if you want to, the
manual flow below works.
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
If you're holding the JWT yourself (e.g. native client, no cookie),
request a fresh challenge and re-sign before expiresAt. 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.