Time-based OTP as a Service

Add 2FA to your app
in minutes

Kanda handles TOTP secret generation, QR codes, and token validation so you don't have to.

Get started free View code examples
How it works

Three steps to secure 2FA

No crypto libraries, no QR code servers. Just a few API calls.

1

Get a token

Register an account or use an API key to exchange for a short-lived JWT.

2

Register a client

Call /api/totp/register. Kanda generates the secret and QR code — show it to your user once.

3

Validate tokens

On every login, call /api/totp/validate with the 6-digit code. Get back {"valid": true}.

4

Monitor access

Every event is logged. Pull the access log from /api/logs or view it in the dashboard.

Code examples

Copy-paste ready snippets for the most common flows.

① Get a JWT (API key exchange)

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']

② Register a TOTP client

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

③ Validate a TOTP token

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")

④ Rotate secret (lost authenticator)

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
API Reference

Endpoints

All endpoints return JSON. Authenticated routes require Authorization: Bearer <token>.

Auth
POST
/api/auth/register
Create a new account. Returns { userId, email }.
Public
POST
/api/auth/login
Login with email + password. Returns { token, expiresIn }.
Public
POST
/api/auth/token
Exchange an API key (kan_…) for a JWT.
Public
TOTP
POST
/api/totp/register
Register a client. Server generates secret + QR data URL (secret shown once).
JWT
POST
/api/totp/validate
Validate a 6-digit TOTP code. Returns { valid: true|false }.
JWT
GET
/api/totp/qrcode/:clientId
Re-fetch the QR data URL for a registered client (secret not included).
JWT
GET
/api/totp
List all registered clients for your tenant.
JWT
DEL
/api/totp/:clientId
Remove a TOTP registration.
JWT
POST
/api/totp/rotate/:clientId
Rotate (reset) a TOTP secret for a lost/stolen device. Generates a fresh secret and QR code — all previous codes are immediately invalidated.
JWT
API Keys
POST
/api/apikeys
Create a new API key. Returns full key once.
JWT
GET
/api/apikeys
List API keys (prefix only, never the full key).
JWT
DEL
/api/apikeys/:id
Revoke an API key immediately.
JWT
Logs
GET
/api/logs?limit=50&offset=0&event=
Paginated access log for your tenant. Filter by event type.
JWT