Web SDK Quickstart
Add passwordless authentication to your web app
Before You Begin
Configure your auth flow first
Before integrating the SDK, set up your project in the Hawcx Admin Console:
- Create a project — Give it a name and select your environment
- Configure authentication methods — Choose your primary MFA (Email OTP, SMS, TOTP, etc.)
- Generate an API key — You'll need the
configIdfor the SDK
How the flow works
- Create a client and call
start(email). - The server responds with steps (select method, enter code, etc.).
- When complete, the SDK returns
authCode+codeVerifier. - Your backend exchanges them for verified claims and creates a session.
Install
npm install @hawcx/reactnpm install @hawcx/coreCreate Client
Wrap your app with HawcxProvider:
// src/App.tsx
import { HawcxProvider } from '@hawcx/react';
export function App() {
return (
<HawcxProvider config={{
configId: 'your_config_id'
}}>
<YourApp />
</HawcxProvider>
);
}Then use hooks in your components:
import { useFlowState, useFlowActions } from '@hawcx/react';
function LoginPage() {
const state = useFlowState();
const { start, selectMethod, submitCode, reset } = useFlowActions();
// ...
}import { createHawcxClient } from '@hawcx/core';
const client = createHawcxClient({
configId: 'your_config_id'
});
// Subscribe to state changes
client.onStateChange((state) => {
renderUI(state);
});Render Flow States
The SDK uses a state machine. Render UI based on state.status:
| Status | What to Show |
|---|---|
idle | Email input form |
loading | Spinner |
prompt | Step-specific UI (status: prompt) |
completed | Success message, send codes to backend |
error | Error message + retry button |
Step Types (prompt.type)
When state.status === 'prompt', render UI based on state.prompt.type:
| Step | User Input | Method to Call |
|---|---|---|
select_method | Pick auth method | selectMethod(methodId) |
enter_code | Enter OTP code | submitCode(code) |
enter_totp | Enter authenticator code | submitTotp(code) |
setup_totp | Scan QR + enter code | submitTotp(code) |
setup_sms | Enter phone number | submitPhone(phone) |
await_approval | Wait (show QR if provided) | SDK polls automatically |
redirect | — | Redirect to prompt.url |
Minimal Flow Example
// src/LoginFlow.tsx
import { useState } from 'react';
import { useFlowState, useFlowActions } from '@hawcx/react';
export function LoginFlow() {
const state = useFlowState();
const { start, selectMethod, submitCode, reset } = useFlowActions();
const [email, setEmail] = useState('');
const [code, setCode] = useState('');
if (state.status === 'idle') {
return (
<form onSubmit={(e) => { e.preventDefault(); start(email); }}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<button type="submit">Continue</button>
</form>
);
}
if (state.status === 'loading') return <div>Loading...</div>;
if (state.status === 'error') {
return (
<div>
<p>Error: {state.error.message}</p>
<button onClick={reset}>Try Again</button>
</div>
);
}
if (state.status === 'completed') {
fetch('/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
authCode: state.authCode,
codeVerifier: state.codeVerifier
})
}).then(() => window.location.href = '/');
return <div>Success! Redirecting...</div>;
}
if (state.status === 'prompt') {
const { prompt } = state;
if (prompt.type === 'select_method') {
return (
<div>
<h3>Choose method</h3>
{prompt.methods.map((m) => (
<button key={m.name} onClick={() => selectMethod(m.name)}>
{m.label}
</button>
))}
</div>
);
}
if (prompt.type === 'enter_code') {
return (
<form onSubmit={(e) => { e.preventDefault(); submitCode(code); }}>
<p>Code sent to {prompt.destination}</p>
<input
value={code}
onChange={(e) => setCode(e.target.value)}
maxLength={prompt.codeLength}
/>
<button type="submit">Verify</button>
</form>
);
}
return <div>Step: {prompt.type}</div>;
}
return null;
}import { createHawcxClient } from '@hawcx/core';
// Replace with your actual values from Step 2
const client = createHawcxClient({
configId: 'your_config_id'
});
// Subscribe to state changes
client.onStateChange(renderUI);
// Start login
function login(email: string) {
client.start(email);
}
function renderUI(state) {
const app = document.getElementById('app')!;
if (state.status === 'idle') {
app.innerHTML = `
<input type="email" id="email" placeholder="Enter your email">
<button onclick="login(document.getElementById('email').value)">Continue</button>
`;
return;
}
if (state.status === 'loading') {
app.innerHTML = '<div>Loading...</div>';
return;
}
if (state.status === 'error') {
app.innerHTML = `
<p>Error: ${state.error.message}</p>
<button onclick="client.reset()">Try Again</button>
`;
return;
}
if (state.status === 'completed') {
fetch('/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
authCode: state.authCode,
codeVerifier: state.codeVerifier
})
}).then(() => window.location.href = '/');
app.innerHTML = '<div>Success! Redirecting...</div>';
return;
}
if (state.status === 'prompt') {
const { prompt } = state;
if (prompt.type === 'select_method') {
app.innerHTML = `
<h3>Choose verification method</h3>
${prompt.methods.map(m =>
`<button onclick="client.selectMethod('${m.name}')">${m.label}</button>`
).join('')}
`;
return;
}
if (prompt.type === 'enter_code') {
app.innerHTML = `
<p>Code sent to ${prompt.destination}</p>
<input id="code" maxlength="${prompt.codeLength}" placeholder="Enter code">
<button onclick="client.submitCode(document.getElementById('code').value)">Verify</button>
`;
return;
}
app.innerHTML = `<div>Step: ${prompt.type}</div>`;
}
}
// Make login available globally for onclick
(window as any).login = login;
(window as any).client = client;Backend Exchange
When state.status === 'completed', send authCode and codeVerifier to your backend for verification and session creation. The endpoint is yours to implement; Hawcx provides the SDK only.
Optional (advanced): If you also manage MFA or step-up on the backend, use HAWCX_SECRET_KEY (a credential blob from the dashboard) with the delegation client. This is separate from the exchange below.
npm install @hawcx/oauth-clientimport { HawcxOAuth } from '@hawcx/oauth-client';
const oauth = new HawcxOAuth({
configId: process.env.HAWCX_API_KEY!
});
// Your backend route (Hawcx does not provide this endpoint)
app.post('/exchange', async (req, res) => {
try {
const { authCode, codeVerifier } = req.body;
const { claims } = await oauth.exchangeCode(authCode, codeVerifier);
// claims.sub = user ID, claims.email = verified email
// Create your own session here
res.json({ success: true, userId: claims.sub });
} catch (error) {
res.status(401).json({ error: 'Authentication failed' });
}
});pip install hawcx-oauth-clientfrom hawcx_oauth_client import HawcxOAuth
import os
oauth = HawcxOAuth(
config_id=os.getenv('HAWCX_API_KEY')
)
@app.route('/exchange', methods=['POST'])
def auth():
try:
data = request.json
result = oauth.exchange_code(data['authCode'], data['codeVerifier'])
# result.claims['sub'] = user ID, result.claims['email'] = verified email
return jsonify({'success': True, 'userId': result.claims['sub']})
except Exception as e:
return jsonify({'error': 'Authentication failed'}), 401Error Handling
FlowError has a category field to help you recover:
if (state.status === 'error') {
switch (state.error.category) {
case 'retryable':
// Transient error - can retry
break;
case 'user_action':
// User needs to do something (wrong code, invalid input)
break;
case 'fatal':
// Unrecoverable - restart flow
client.reset();
break;
}
}Handling All Steps
The minimal example handles select_method and enter_code. Here are the rest:
// Add these handlers to your LoginFlow component
// TOTP verification
if (prompt.type === 'enter_totp') {
return (
<form onSubmit={(e) => { e.preventDefault(); submitTotp(code); }}>
<input value={code} onChange={(e) => setCode(e.target.value)} placeholder="Authenticator code" />
<button type="submit">Verify</button>
</form>
);
}
// TOTP setup (show QR code)
if (prompt.type === 'setup_totp') {
return (
<div>
<h3>Set up authenticator</h3>
<img src={`https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(prompt.otpauthUrl)}`} alt="QR Code" />
<p>Or enter manually: <code>{prompt.secret}</code></p>
<form onSubmit={(e) => { e.preventDefault(); submitTotp(code); }}>
<input value={code} onChange={(e) => setCode(e.target.value)} placeholder="Enter code from app" />
<button type="submit">Verify</button>
</form>
</div>
);
}
// SMS setup
if (prompt.type === 'setup_sms') {
const [phone, setPhone] = useState('');
return (
<form onSubmit={(e) => { e.preventDefault(); submitPhone(phone); }}>
<input value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="+1234567890" />
<button type="submit">Send Code</button>
</form>
);
}
// Push/QR approval (SDK polls automatically)
if (prompt.type === 'await_approval') {
return (
<div>
{prompt.qrData && <img src={`https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(prompt.qrData)}`} alt="QR Code" />}
<p>Waiting for approval...</p>
</div>
);
}
// External OAuth redirect
if (prompt.type === 'redirect') {
window.location.href = prompt.url;
return <div>Redirecting...</div>;
}// Add these cases to your renderUI function
if (prompt.type === 'enter_totp') {
app.innerHTML = `
<input id="totp" placeholder="Authenticator code">
<button onclick="client.submitTotp(document.getElementById('totp').value)">Verify</button>
`;
return;
}
if (prompt.type === 'setup_totp') {
app.innerHTML = `
<h3>Set up authenticator</h3>
<img src="https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(prompt.otpauthUrl)}" alt="QR">
<p>Manual: <code>${prompt.secret}</code></p>
<input id="totp" placeholder="Code from app">
<button onclick="client.submitTotp(document.getElementById('totp').value)">Verify</button>
`;
return;
}
if (prompt.type === 'setup_sms') {
app.innerHTML = `
<input id="phone" placeholder="+1234567890">
<button onclick="client.submitPhone(document.getElementById('phone').value)">Send Code</button>
`;
return;
}
if (prompt.type === 'await_approval') {
app.innerHTML = `
${prompt.qrData ? `<img src="https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(prompt.qrData)}" alt="QR">` : ''}
<p>Waiting for approval...</p>
`;
return;
}
if (prompt.type === 'redirect') {
window.location.href = prompt.url;
return;
}Quick Reference
Flow States
| Status | Meaning | Your Action |
|---|---|---|
idle | Ready to start | Show email input |
loading | Processing | Show spinner |
prompt | Server needs input | Render the step |
completed | Auth successful | Send authCode + codeVerifier to backend |
error | Something failed | Show error, offer retry |
Flow Types
Most apps can omit the flow type — start(email) auto-detects new vs returning users.
| Type | When to Use |
|---|---|
signup | Force new-user registration |
account_manage | Step-up auth for sensitive actions |
SDK Methods
| Method | When to Call |
|---|---|
start(email) | Begin authentication |
selectMethod(methodId) | After select_method step |
submitCode(code) | After enter_code step |
submitTotp(code) | After enter_totp or setup_totp step |
submitPhone(phone) | After setup_sms step |
resend() | Resend OTP code |
Next Steps
- API Reference — All methods, types, and step types
- 5-Minute Guide — End-to-end minimal example
- MFA & Step-Up — Higher-assurance flows
- Troubleshooting — Common issues