API Documentation
/
Frontend
/
Web
/
Web SDK Quickstart

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:

  1. Create a project — Give it a name and select your environment
  2. Configure authentication methods — Choose your primary MFA (Email OTP, SMS, TOTP, etc.)
  3. Generate an API key — You'll need the configId for the SDK

See Projects and API Keys for detailed steps.


How the flow works

  1. Create a client and call start(email).
  2. The server responds with steps (select method, enter code, etc.).
  3. When complete, the SDK returns authCode + codeVerifier.
  4. Your backend exchanges them for verified claims and creates a session.

Install

npm install @hawcx/react
npm install @hawcx/core

Create 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:

StatusWhat to Show
idleEmail input form
loadingSpinner
promptStep-specific UI (status: prompt)
completedSuccess message, send codes to backend
errorError message + retry button

Step Types (prompt.type)

When state.status === 'prompt', render UI based on state.prompt.type:

StepUser InputMethod to Call
select_methodPick auth methodselectMethod(methodId)
enter_codeEnter OTP codesubmitCode(code)
enter_totpEnter authenticator codesubmitTotp(code)
setup_totpScan QR + enter codesubmitTotp(code)
setup_smsEnter phone numbersubmitPhone(phone)
await_approvalWait (show QR if provided)SDK polls automatically
redirectRedirect 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-client
import { 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-client
from 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'}), 401

Error 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

StatusMeaningYour Action
idleReady to startShow email input
loadingProcessingShow spinner
promptServer needs inputRender the step
completedAuth successfulSend authCode + codeVerifier to backend
errorSomething failedShow error, offer retry

Flow Types

Most apps can omit the flow type — start(email) auto-detects new vs returning users.

TypeWhen to Use
signupForce new-user registration
account_manageStep-up auth for sensitive actions

SDK Methods

MethodWhen 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