Kanda handles TOTP secret generation, QR codes, and token validation so you don't have to.
No crypto libraries, no QR code servers. Just a few API calls.
Register an account or use an API key to exchange for a short-lived JWT.
Call /api/totp/register. Kanda generates the secret and QR code — show it to your user
once.
On every login, call /api/totp/validate with the 6-digit code. Get back
{"valid": true}.
Every event is logged. Pull the access log from /api/logs or view it in the dashboard.
Copy-paste ready snippets for the most common flows.
curl -X POST https://your-kanda-server/api/auth/token \ -H "Content-Type: application/json" \ -d '{"apiKey": "kan_your_api_key_here"}' # Response { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expiresIn": "1h" }
const getToken = async () => { const res = await fetch('https://your-kanda-server/api/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: 'kan_your_api_key_here' }), }); const { token } = await res.json(); return token; };
import requests res = requests.post( 'https://your-kanda-server/api/auth/token', json={'apiKey': 'kan_your_api_key_here'} ) token = res.json()['token']
curl -X POST https://your-kanda-server/api/totp/register \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"clientId":"user-123","label":"alice@example.com","issuer":"MyApp"}' # Response — secret shown ONCE { "clientId": "user-123", "secret": "JBSWY3DPEHPK3PXP", "otpauthUrl": "otpauth://totp/alice...", "qrDataUrl": "data:image/png;base64,..." }
const res = await fetch('https://your-kanda-server/api/totp/register', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ clientId: 'user-123', label: 'alice@example.com', issuer: 'MyApp', }), }); const { secret, qrDataUrl } = await res.json(); // Show qrDataUrl as <img src={qrDataUrl}> — let user scan it // Store nothing — Kanda stores the secret for you
res = requests.post( 'https://your-kanda-server/api/totp/register', headers={'Authorization': f'Bearer {token}'}, json={ 'clientId': 'user-123', 'label': 'alice@example.com', 'issuer': 'MyApp', } ) data = res.json() qr_data_url = data['qrDataUrl'] # render as <img> for user to scan
curl -X POST https://your-kanda-server/api/totp/validate \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"clientId":"user-123","token":"847291"}' # Response { "valid": true }
const validate2FA = async (clientId, userOtpCode) => { const res = await fetch('https://your-kanda-server/api/totp/validate', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ clientId, token: userOtpCode }), }); const { valid } = await res.json(); return valid; // true or false };
res = requests.post( 'https://your-kanda-server/api/totp/validate', headers={'Authorization': f'Bearer {token}'}, json={'clientId': 'user-123', 'token': otp_from_user} ) valid = res.json()['valid'] # True or False if not valid: raise Exception("Invalid OTP")
Call this when a customer loses access to their authenticator app. A brand-new secret is generated and the old one is instantly invalidated.
curl -X POST https://your-kanda-server/api/totp/rotate/user-123 \ -H "Authorization: Bearer $TOKEN" # Response — new secret & QR, old codes are dead { "clientId": "user-123", "secret": "NEWBASE32SECRET", "otpauthUrl": "otpauth://totp/alice...", "qrDataUrl": "data:image/png;base64,..." }
const rotateTotp = async (clientId) => { const res = await fetch(`https://your-kanda-server/api/totp/rotate/${clientId}`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, }); const { qrDataUrl } = await res.json(); // Show qrDataUrl as <img src={qrDataUrl}> — let user scan new QR return qrDataUrl; };
res = requests.post( f'https://your-kanda-server/api/totp/rotate/{client_id}', headers={'Authorization': f'Bearer {token}'} ) data = res.json() qr_data_url = data['qrDataUrl'] # render as <img> for user to scan
All endpoints return JSON. Authenticated routes require Authorization: Bearer <token>.
{ userId, email }.{ token, expiresIn }.kan_…) for a JWT.{ valid: true|false }.