A transfer request from start to finish — build the URI, hand off to Sigil, receive the result via server POST.
import { buildUri } from '@sigil-oss/connect';
// 1. Store the nonce so you can verify it in the callback
const nonce = crypto.randomUUID();
sessionStorage.setItem('pending_nonce', nonce);
// 2. Build the URI
const uri = buildUri({
request: {
type: 'transfer',
nonce,
dapp: { name: 'Acme', origin: 'https://acme.example' },
to: 'NQZBXKZP4MTLDUVWXYZK8MFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
amount: 1_000_000,
exp: Math.floor(Date.now() / 1000) + 300, // 5 minutes
},
callback: 'https://acme.example/api/sigil/callback',
});
// 3. Hand off to Sigil
window.location.href = uri;// POST https://acme.example/api/sigil/callback
app.post('/api/sigil/callback', express.json(), (req, res) => {
const { status, type, nonce, ...rest } = req.body;
// Reject unknown nonces immediately
if (!pendingNonces.has(nonce)) {
return res.sendStatus(400);
}
pendingNonces.delete(nonce);
switch (status) {
case 'signed':
// type === 'transfer' | 'sc_call'
handleSigned(rest.identity, rest.tx_hash, rest.target_tick);
break;
case 'connected':
handleConnect(rest.identity, rest.permissions);
break;
case 'signed': // sign_message
handleSignature(rest.identity, rest.signature, rest.public_key);
break;
case 'verified':
handleVerify(rest.valid, rest.identity);
break;
case 'rejected':
handleRejection(rest.reason);
break;
}
res.sendStatus(200);
});Static site or SPA with no server — result delivered as a query parameter when Sigil redirects the browser back to your page.
// Static site / SPA — no server required
function b64url(str: string): string {
return btoa(str).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');
}
// 1. Build the request, store nonce for verification after redirect
const nonce = crypto.randomUUID();
sessionStorage.setItem('sigil_nonce', nonce);
const envelope = {
request: {
type: 'sign_message',
nonce,
dapp: { name: 'Acme', origin: 'https://acme.example' },
message: 'Sign in to Acme · ' + new Date().toISOString(),
},
redirect_uri: 'https://acme.example/signed', // Sigil opens this when done
};
window.location.href = 'sigil://v1/request?d=' + b64url(JSON.stringify(envelope));
// 2. On https://acme.example/signed — parse the result
const encoded = new URLSearchParams(location.search).get('result');
if (encoded) {
const result = JSON.parse(atob(encoded.replace(/-/g, '+').replace(/_/g, '/')));
const storedNonce = sessionStorage.getItem('sigil_nonce');
if (result.nonce !== storedNonce) throw new Error('nonce mismatch');
if (result.status === 'signed') {
console.log('Signed by:', result.identity);
console.log('Signature:', result.signature);
} else {
console.log('Rejected:', result.reason);
}
}| reason | Meaning |
|---|---|
user_rejected | The user tapped Reject or closed the review screen |
expired | The request's exp timestamp passed before the user acted |
wallet_locked | Wallet was locked and the user did not unlock in time (queue timeout) |
These cause Sigil to silently discard the request before showing it to the user. Check the Rust validator at src-tauri/src/deep_link.rs for the canonical list.
| Condition | Cause |
|---|---|
| Envelope too large | Base64-encoded d exceeds 8 192 bytes |
| Invalid scheme / path | URI is not sigil://v1/request?d=… |
| Nonce too short / too long | Must be 16–128 chars, alphanumeric or -_=+ |
| Nonce replay | Same nonce seen within the last hour |
Missing dapp.origin | dapp.origin must be a valid HTTPS URL |
exp too far ahead | More than 1 hour from the current time |
| Callback not HTTPS | Production callbacks must be https://; only http://localhost and http://127.0.0.1 are allowed for dev |
| Level | Blocks? |
|---|---|
legacy_unverified | No — shown with a warning badge |
signed_untrusted | No — valid signature, issuer not in user's registry |
verified_registry | No — fully verified |
signature_invalid | Yes — proof present but signature check failed |
registry_revoked | Yes |
registry_origin_mismatch | Yes — registered issuer but wrong origin |
If you'd rather not add a dependency, the whole protocol fits in a handful of lines.
// Without the SDK — matches what buildUri() does internally
function b64url(str: string): string {
return btoa(str)
.replaceAll('+', '-')
.replaceAll('/', '_')
.replaceAll('=', '');
}
const envelope = {
request: {
type: 'sign_message',
nonce: crypto.randomUUID(),
dapp: { name: 'Acme', origin: 'https://acme.example' },
message: 'Sign in to Acme · ' + new Date().toISOString(),
},
callback: 'https://acme.example/api/sigil/callback',
};
const uri = 'sigil://v1/request?d=' + b64url(JSON.stringify(envelope));
window.location.href = uri;