Web SDK API Reference

Complete API reference for Hawcx Web SDK

Factory

createHawcxAuth(config)

Creates a Hawcx client instance.

import { createHawcxAuth } from '@hawcx/core';

const client = createHawcxAuth({
  configId: 'your_config_id',
  apiBase: '<Base URL from Admin Console>/v1'
});

Config (AuthConfig):

PropertyTypeRequiredDescription
configIdstringYesYour Hawcx Config ID
apiBasestringNoYour tenant API URL (default https://api.hawcx.com/v1)
loggerLoggerNoLogger for debugging (default silent)
fetchtypeof fetchNoCustom fetch implementation (for testing)
timeoutnumberNoRequest timeout in ms (default 10000)

Returns: HawcxAuth

Logging

Logger is an interface with optional debug / info / warn / error methods, each (message: string, data?: unknown) => void. Two ready-made instances are exported: noopLogger (silent, the default) and consoleLogger (forwards to the browser console).

import { createHawcxAuth, consoleLogger } from '@hawcx/core';

const client = createHawcxAuth({ configId: '...', logger: consoleLogger });

A StorageAdapter interface (getItem / setItem / removeItem, same shape as localStorage) is also exported for typing custom storage backends.


State Machine

The client maintains an AuthState that drives your UI. Subscribe to changes:

client.onStateChange((state) => {
  // Render UI based on state.status
});

// Or get current state
const state = client.getState();

Flow States

AuthState is a union of five variants, each exported as its own type:

StatusTypeShapeDescription
idleAuthStateIdle{ status: 'idle' }Ready to start
loadingAuthStateLoading{ status: 'loading', session?, notice? }Processing request
stepAuthStateStep{ status: 'step', session, step: AuthStep, notice? }Server needs user input
completedAuthStateCompleted{ status: 'completed', session, authCode, expiresAt, sessionToken? }Auth successful
errorAuthStateError{ status: 'error', session?, error: AuthError, previousStep? }Something failed

previousStep on the error state preserves the step the user was on for retryable errors (e.g. a wrong TOTP code), so your form can stay in place.

Completed State & AuthResult

interface AuthStateCompleted {
  status: 'completed';
  session: string;
  authCode: string;       // Send to backend
  expiresAt: string;      // ISO timestamp
  sessionToken?: string;  // hx_auth session JWT (bearer for self-service device calls), if minted
}

The PKCE codeVerifier is not on the state — the SDK holds it locally from flow start. Read the full completion via client.getCompletion() (or useSession() in React), which returns an AuthResult:

interface AuthResult {
  authCode: string;       // Send to backend
  expiresAt: string;      // ISO timestamp
  codeVerifier?: string;  // Send to backend (PKCE)
  sessionToken?: string;  // hx_auth session JWT — bearer for logoutEverywhere() / self-service device calls
}

sessionToken (added in 2.6.0) is the hx_auth session JWT, present only when the server minted one. Pass it as the bearer to logoutEverywhere() (or React's logoutAllDevices) for single-logout / self-service device management.

codeVerifier is optional because it lives only in the client instance that called start(). Read it with client.getCompletion() as soon as the flow completes and send it to your backend alongside authCode — a different client instance (for example one created after a full page navigation) will not have it, and token exchange fails without it.

AuthError

interface AuthError {
  code: string;                    // Error code
  message: string;                 // Human-readable message
  category: AuthErrorCategory;     // Recovery hint
  details?: Record<string, unknown>;
}

enum AuthErrorCategory {
  RETRYABLE = 'retryable',
  USER_ACTION = 'user_action',
  FATAL = 'fatal',
}

AuthErrorCategory is a runtime enum — compare with AuthErrorCategory.RETRYABLE etc., or against the string values shown.

CategoryMeaningRecovery
retryableTransient error (network, rate limit)Retry the same action
user_actionUser error (wrong code, invalid input)Show error, let user retry
fatalUnrecoverable (session expired)Call reset() and start over

State Helpers & Type Guards

@hawcx/core also exports standalone helper functions for narrowing an AuthState outside React (in React, useAuthFlags() covers the common cases):

HelperReturns
isIdle(state) / isLoading(state) / isAuthStep(state) / isCompleted(state) / isError(state)boolean — narrows state to the matching variant
getStepType(state)The current step's type string, or null if not in a step
getMethods(state)The select_method step's methods array, or []
getAuthError(state)AuthError | null
getAuthNotice(state)AuthNotice | null — the server-driven notice on the current step (see AuthNotice)

Per-step type guards narrow state.step once you know state.status === 'step': isSelectMethodStep, isEnterCodeStep, isEnterTotpStep, isSetupTotpStep, isSetupSmsStep, isDeviceChallengeStep, isRedirectStep, isAwaitApprovalStep, isCompletedStep, isErrorStep.

import { isAuthStep, isEnterCodeStep } from '@hawcx/core';

if (isAuthStep(state) && isEnterCodeStep(state.step)) {
  console.log(state.step.destination); // fully typed
}

AuthNotice (device revocation)

A notice is a non-fatal, server-driven message that rides along with the current step. Unlike AuthError, it does not end the flow.

interface AuthNotice {
  code: string;     // e.g. 'unauthorized_device'
  message: string;  // Human-readable, safe to display
}

When an admin revokes a device in the Admin Console, that device's next sign-in returns 403 unauthorized_device. The SDK auto-recovers: it clears the revoked credential and replays the flow once, skipping device trust so the user re-verifies (OTP) and the device re-enrolls. No error is thrown — instead a notice rides along on the verification step:

{
  "code": "unauthorized_device",
  "message": "This device's access was revoked. Verify your identity to continue."
}

notice is declared only on the loading and step states, so narrow the state before reading it:

const notice =
  state.status === 'step' || state.status === 'loading'
    ? state.notice ?? null
    : null;

Render the notice as a banner above your form; the user re-verifies once and continues. The drop-in HawcxSignUpSignIn component (see React Components) does this automatically.


Steps

When state.status === 'step', the server is asking for user input. Each step type below is exported as its own interface, with a matching type guard. AuthStep is the union of all steps and StepType is the union of their type strings — note both unions also include one SDK-internal member, setup_device, whose interface and guard are not individually exported (see the device_challenge section below).

select_method (SelectMethodStep)

User chooses an authentication method.

interface SelectMethodStep {
  type: 'select_method';
  phase: 'primary' | 'mfa' | 'enrollment';  // Which phase of the flow
  methods: Method[];
}

interface Method {
  name: string;   // Method ID to pass to selectMethod()
  label: string;  // Display label
  icon?: string;  // Icon identifier
}

Action: Call selectMethod(method.name)


enter_code (EnterCodeStep)

User enters an OTP code sent via email or SMS.

interface EnterCodeStep {
  type: 'enter_code';
  destination: string;    // Masked email/phone, e.g. "s***@example.com"
  channel?: 'sms' | 'email';
  codeLength: number;     // Expected code length
  codeFormat: 'numeric' | 'alphanumeric';  // For keyboard hints
  codeExpiresAt: string;  // ISO 8601
  resendAt: string;       // When resend becomes available (ISO 8601)
}

Action: Call submitCode(code)


enter_totp (EnterTotpStep)

User enters a code from their authenticator app.

interface EnterTotpStep {
  type: 'enter_totp';
}

Action: Call submitTotp(code)


setup_totp (SetupTotpStep)

User sets up TOTP for the first time.

interface SetupTotpStep {
  type: 'setup_totp';
  secret: string;         // Base32 manual entry key
  otpauthUrl: string;     // otpauth:// URL for QR code
  period: number;         // TOTP period in seconds (typically 30)
  backupCodes?: string[]; // One-time recovery codes — show once
}

UI: Show QR code from otpauthUrl, display secret for manual entry. If backupCodes is present, display them once and prompt the user to save them.

Action: Call submitTotp(code) after user enters code from authenticator app.


setup_sms (SetupSmsStep)

User enrolls their phone number.

interface SetupSmsStep {
  type: 'setup_sms';
  existingPhone?: string;  // Masked existing phone being replaced, if any
}

Action: Call submitPhone(phoneNumber)


await_approval (AwaitApprovalStep)

Waiting for external approval (QR scan, push notification).

interface AwaitApprovalStep {
  type: 'await_approval';
  qrData?: string;      // QR code data (if applicable)
  expiresAt: string;    // When approval expires (ISO 8601)
  pollInterval: number; // Suggested poll interval in seconds
}

UI: Show QR code if qrData provided, otherwise show "Waiting for approval..."

Action: None. The SDK polls automatically and transitions when approved.


redirect (RedirectStep)

Redirect to external OAuth provider. This is the SDK-embedded pattern's handoff when Social/SSO is enabled — it is not the OIDC /oauth2/token flow. See Choose your integration pattern to reconcile the three redirect flows.

interface RedirectStep {
  type: 'redirect';
  url: string;           // Full redirect URL
  returnScheme?: string; // Deep link scheme for mobile
  nonce?: string;        // Token mode (One Tap): session-bound OIDC nonce
  clientId?: string;     // Token mode: upstream IdP client_id
}

Action: window.location.href = step.url


device_challenge (DeviceChallengeStep) — SDK-internal

Device trust verification. Handled automatically by the SDK — never render it. When the server recognizes an enrolled device it returns this step; the SDK loads the device key from secure storage, signs the challenge, and submits without user interaction. It is exported (with isDeviceChallengeStep) so custom step-handling code can recognize and skip it.

interface DeviceChallengeStep {
  type: 'device_challenge';
  challenge: string;            // Challenge bytes to sign
  challengeEncoding: 'base64';
  keyId: string;                // Key ID to sign with
  algorithm: 'Ed25519';
  domain: string;               // Domain for signature binding
  combinedsalt: string;         // Base64 salt for key unwrap
  responseEncoding: 'base64';
}

A sibling setup_device step (device enrollment) is likewise SDK-internal and handled automatically. Unlike the steps above, its interface (SetupDeviceStep) and guard are not exported from the package — if you write an exhaustive switch over StepType, handle 'setup_device' with a pass-through (the SDK answers it before your UI ever sees it in practice).


completed (CompletedStep) and error (ErrorStep)

These two close out the flow. The SDK consumes them and surfaces state.status === 'completed' / 'error' instead, so most apps never handle them as steps — the types matter when working with client.send() or previousStep directly.

interface CompletedStep {
  type: 'completed';
  authCode: string;   // Authorization code
  expiresAt: string;  // ISO 8601
}

interface ErrorStep {
  type: 'error';
  code: string;          // Machine-readable code (for logging, not branching)
  // Server-driven recovery action — BRANCH ON THIS. The open `(string & {})`
  // union means newer servers may send actions not in ErrorAction.
  action: ErrorAction | (string & {});
  message: string;       // Human-readable message
  retryable: boolean;
  details?: ErrorDetails;
}

type ErrorAction =
  | 'retry_input'    // Wrong input, let user try again
  | 'restart_flow'   // Session invalid, start over
  | 'wait'           // Rate limited — wait details.retry_after_seconds
  | 'retry_request'  // Transient failure — safe to retry the same action
  | 'abort'          // Unrecoverable, show error
  | 'resend_code'    // Code expired, offer resend
  | 'select_method'; // Go back to method selection

interface ErrorDetails {
  retry_after_seconds?: number;                      // For 'wait'
  errors?: Array<{ field: string; message: string }>; // Validation failures
  [key: string]: unknown;
}

Branch on action, not code — unknown actions from newer servers should fall back to showing message.


Wire Protocol Types

For advanced use (custom transports, request logging, client.send()), the raw protocol v2 types are exported:

ExportDescription
PROTOCOL_VERSIONThe protocol version constant (2)
WireRequestClient → server envelope: { protocolVersion, session?, action }
WireResponseServer → client envelope: { protocolVersion, session, step, meta }
ResponseMeta{ traceId, expiresAt } — tracing id and session expiry
ActionUnion of all client actions (below)
AuthStep / StepTypeUnion of all steps / of their type strings
DeviceInfoDevice fingerprint payload sent with start: { v: 1, fingerprint, signals? }
AuthMode'signin' | 'signup' | 'account_manage'

Action types — one interface per action, discriminated by type: StartAction (start), SelectMethodAction (select_method), SubmitCodeAction (submit_code), SubmitTotpAction (submit_totp), SubmitPhoneAction (submit_phone), SubmitSignatureAction (submit_signature, device trust — SDK-internal), OAuthCallbackAction (oauth_callback, social login return leg), ResendAction (resend), PollAction (poll), CancelAction (cancel). Each client method documented under Methods sends the corresponding action.

The Action union has one more member: request_challenge (device-enrollment keyset upload, sent automatically by the SDK after a setup_device step). Like SetupDeviceStep, its interface is SDK-internal and not individually exported — an exhaustive switch over action.type in a custom transport or request logger must still expect it.


Methods

start(identifier, flowType?)

Begin an authentication flow.

client.start('user@example.com');
ParameterTypeDescription
identifierstringUser email
flowTypeAuthMode'signin' | 'signup' | 'account_manage'Optional flow override

If flowType is omitted, the SDK defaults to 'signin'. Whether an unknown user is then moved into a signup flow is decided server-side by tenant policy (unknown_user), not by the SDK — on tenants configured to reject unknown users, a plain start(email) for a new user ends in an error state.


selectMethod(methodId)

Select an authentication method after select_method step.

client.selectMethod('email_otp');
ParameterTypeDescription
methodIdstringMethod name from step

submitCode(code)

Submit OTP code after enter_code step.

client.submitCode('123456');
ParameterTypeDescription
codestringOTP code

submitTotp(code)

Submit TOTP code after enter_totp or setup_totp step.

client.submitTotp('123456');
ParameterTypeDescription
codestringAuthenticator code

submitPhone(phoneNumber)

Submit phone number after setup_sms step.

client.submitPhone('+15551234567');
ParameterTypeDescription
phoneNumberstringPhone number (E.164 format)

resend()

Resend OTP code. Valid after enter_code step.

client.resend();

reset()

Clear state and return to idle. Use to start over or recover from fatal errors.

client.reset();

cancel()

Cancel the current flow.

client.cancel();

getState()

Get the current flow state.

const state = client.getState();

onStateChange(callback)

Subscribe to state changes.

const unsubscribe = client.onStateChange((state) => {
  console.log('State changed:', state.status);
});

// Later: unsubscribe();

send(action)

Send a raw protocol action to the server. This is the primitive every convenience method above wraps — use it for actions that have no wrapper, such as the oauth_callback return leg of social login.

const state = await client.send({ type: 'oauth_callback', code, state: csrfState });

getCompletion()

Get the completion result (AuthResult | null). Returns the result only while the client is in the completed state; see Completed State & AuthResult.

const result = client.getCompletion();
if (result) sendToBackend(result.authCode, result.codeVerifier);

onCompletion(callback)

Subscribe to completion changes. Fires with an AuthResult when a flow completes, and with null when the session is cleared (e.g. signOut()) — always handle the null case.

const unsubscribe = client.onCompletion((result) => {
  if (result) onSignedIn(result);
  else onSignedOut();
});

signOut()

Reset to idle and clear the completion (notifies onCompletion listeners with null).

client.signOut();

logoutEverywhere(bearerToken)

End all active sessions for the user (single-logout across every device), not just this one. Added in 2.6.0. Pass the sessionToken from the completed AuthResult as the bearer.

const result = client.getCompletion();
if (result?.sessionToken) {
  const { sessionsEnded } = await client.logoutEverywhere(result.sessionToken);
  console.log(`ended ${sessionsEnded} sessions`);
}

Returns: Promise<{ sessionsEnded: number }>. In React, use useAuthActions().logoutAllDevices, which wraps this.


getSessionId() / getConfigId() / hasDeviceCredentials(identifier)

MethodReturnsDescription
getSessionId()string | undefinedCurrent flow session id (for multi-step flows)
getConfigId()stringThe configured Config ID
hasDeviceCredentials(identifier)Promise<boolean>Whether this browser has stored device-trust credentials for the identifier

resumeSession(session, identifier?) / getPkceVerifier()

Carry a flow across a full page navigation (e.g. a social-login redirect). Before redirecting, persist getSessionId() and getPkceVerifier(); after returning, adopt the flow on the fresh client instance with resumeSession(...).

// before redirecting:
sessionStorage.setItem('hx', JSON.stringify({
  session: client.getSessionId(),
  verifier: client.getPkceVerifier(),   // string | null — persist so token exchange still works
}));

// after returning, on a new client instance:
const { session, verifier } = JSON.parse(sessionStorage.getItem('hx')!);
client.resumeSession(session);           // resumeSession(session: string, identifier?: string): void
MethodSignatureNotes
resumeSession(session, identifier?)(session: string, identifier?: string) => voidRe-attach a fresh client to an in-progress flow. Pass identifier when the resumed leg may enroll a device.
getPkceVerifier()() => string | nullThe PKCE verifier for the current flow. Persist it before a redirect — a resumed client instance won't have it, and token exchange fails without it.

Utilities

getDeviceInfo()

Read basic device information from the browser. Exported from @hawcx/core.

import { getDeviceInfo } from '@hawcx/core';

const { bui, osType } = getDeviceInfo();

Returns:

PropertyTypeDescription
buistringBrowser user information (the user agent string)
osTypestringOperating system, from navigator.userAgentData.platform with navigator.platform fallback

Both fields fall back to 'unknown' outside a browser environment.

Note: this return shape is not the DeviceInfo wire type ({ v, fingerprint, signals }) that appears in protocol payloads — that type describes what the SDK sends to the server; getDeviceInfo() is a small client-side convenience.


React Components

Prebuilt components from @hawcx/react. All of them must be rendered inside a HawcxProvider. For the prebuilt styling, import the stylesheet once:

import '@hawcx/react/dist/styles.css';

<HawcxSignUpSignIn />

The drop-in authentication component — the fastest path to a working integration. It renders the entire flow for you: identifier form, method selection, OTP / TOTP entry, TOTP and SMS enrollment (including QR codes), QR / push approval waiting, risk verification, and error states. You only handle the result.

import { HawcxProvider, HawcxSignUpSignIn } from '@hawcx/react';
import '@hawcx/react/dist/styles.css';

function LoginPage() {
  return (
    <HawcxProvider config={{ configId: '...', apiBase: '...' }}>
      <HawcxSignUpSignIn
        onSuccess={({ authCode, codeVerifier }) => {
          // Send both to your backend for code exchange
        }}
      />
    </HawcxProvider>
  );
}

Props (HawcxSignUpSignInProps):

PropTypeRequiredDescription
onSuccess(result: AuthResult) => voidNoCalled when authentication completes. result contains authCode and codeVerifier to send to your backend
classNamestringNoExtra class(es) appended to the root hawcx-auth-container element

Known issue (through SDK 2.6.0): the component forwards completion notifications to onSuccess unguarded, so calling client.signOut() elsewhere while it is mounted invokes onSuccess(null). Guard against a null result if your app can sign out with the component on screen.

The component ships with sensible built-in behavior: resend cooldown (30 s), device-revocation notice banners, and a risk dialog when the server flags a risky sign-in. Use it when you want a working login screen now; drop down to the form hooks when you need your own markup.


<RiskDetectedDialog />

The verification prompt shown when the server flags a sign-in as risky (new device, unusual location, impossible travel, …). HawcxSignUpSignIn renders it automatically; it is exported for custom UIs that want the same screen. Internal risk-reason codes are mapped to user-friendly text before display.

The risk payload rides on the current step as an untyped wire field — no exported step interface declares it, so read it with a cast (the SDK's own component does the same):

import { RiskDetectedDialog } from '@hawcx/react';

// `risk` is not part of the exported AuthStep types:
const risk = state.status === 'step'
  ? (state.step as any).risk as
      | { detected?: boolean; reasons?: string[];
          location?: { city: string | null; country: string | null };
          message?: string }
      | undefined
  : undefined;

{risk?.detected && (
  <RiskDetectedDialog
    reasons={risk.reasons ?? []}
    location={risk.location ?? { city: null, country: null }}
    message={risk.message ?? 'For your security, we need to verify your identity.'}
    onNext={() => proceedWithVerification()}
    onCancel={() => cancelFlow()}
  />
)}

Props (RiskDetectedDialogProps):

PropTypeRequiredDescription
reasonsstring[]YesRisk reason codes from the step's risk payload
location{ city: string | null; country: string | null }YesDetected sign-in location, rendered as "City, Country". If both fields are null the location row is omitted entirely
messagestringYesExplanatory message shown to the user
onNext() => voidYesUser chose to continue with verification
onCancel() => voidYesUser dismissed — typically cancel the flow

<QrCode />

Renders a string as a QR code image. Used internally for setup_totp (otpauthUrl) and await_approval (qrData) steps; exported for custom UIs.

import { QrCode } from '@hawcx/react';

<QrCode value={step.otpauthUrl} pixelSize={220} />

Props:

PropTypeRequiredDescription
valuestringYesContent to encode
pixelSizenumberNoRendered width/height in px (default 220)
data-testidstringNoTest id on the image (default hawcx-qr)

Shows a "Loading..." placeholder of the same size while the image is generated.


React Hooks

HawcxProvider

Wrap your app to provide the client context.

import { HawcxProvider } from '@hawcx/react';

<HawcxProvider config={{ configId: '...', apiBase: '...' }}>
  <App />
</HawcxProvider>

Instead of config, you can pass an existing instance via client (e.g. one created with createHawcxAuth). At least one of the two must be provided (the provider throws if both are missing); if both are given, client takes precedence and config is ignored.


useAuthState()

Get the current flow state. Re-renders on state changes.

const state = useAuthState();

useAuthFlags()

Derived boolean flags for the current state (returns AuthFlags). Saves you from matching on status yourself, and surfaces any server-driven notice.

const {
  isIdle, isLoading, isStep, isCompleted, isError,
  notice,         // AuthNotice | null — non-fatal message on the current step
  deviceRevoked,  // convenience: true when notice.code === 'unauthorized_device'
} = useAuthFlags();

notice saves you from narrowing the state yourself, and deviceRevoked is the convenience flag for the revoked-device case (see AuthNotice). Render it as a banner above your form — the SDK auto-recovers, so the user just re-verifies once:

const { notice, deviceRevoked } = useAuthFlags();

return (
  <>
    {notice && <Banner tone={deviceRevoked ? 'warn' : 'info'}>{notice.message}</Banner>}
    <SignInForm />
  </>
);

useAuthActions()

Get action methods. Each wraps the corresponding client method.

const {
  send,          // raw protocol action (e.g. oauth_callback)
  start,
  selectMethod,
  submitCode,
  submitTotp,
  submitPhone,
  resend,
  reset,
  cancel,
  signOut,
  logoutAllDevices,  // (bearerToken) => Promise<{ sessionsEnded: number }>
} = useAuthActions();

logoutAllDevices(bearerToken) ends every active session for the user (single-logout) and returns how many were ended — wraps the client's logoutEverywhere. Pass the sessionToken from the completed AuthResult as the bearer.


useAuthClient()

Get the underlying client instance.

const client = useAuthClient();

useSession()

Access the completion result after authentication finishes. Unlike useAuthState(), which tracks the step-by-step flow, useSession() answers "has this client completed auth?" and re-renders on completion changes (including signOut()).

const { session, actions } = useSession();

if (session) {
  // session is an AuthResult: authCode, codeVerifier, expiresAt
  return <button onClick={actions.signOut}>Sign out</button>;
}

Returns:

PropertyTypeDescription
sessionAuthResult | nullCompletion result of this client instance, or null
isLoadingbooleanReserved — currently always false (there is no async hydration phase)
errorAuthError | undefinedReserved — currently never set
actions.signOut() => voidClear the session on this client

Scope: the session is in-memory state of the current client instance — it does not survive a page reload, and there is no cross-tab persistence (the SDK writes no session storage for other tabs to observe). To keep users signed in across reloads, exchange the AuthResult with your backend and use your own application session.


Form Hooks

The form hooks wrap useAuthActions() with ready-made input state: value, setter, trimming, empty-input guard, and an isSubmitting flag. Use them when building a custom UI so you don't rewire the same form plumbing; use useAuthActions() directly when you need full control. HawcxSignUpSignIn is built from these same hooks.

useIdentifierForm(options)

State for the identifier (email) input that starts the flow.

const { identifier, setIdentifier, isSubmitting, submit } =
  useIdentifierForm({ flowType: 'signin' });

<form onSubmit={(e) => { e.preventDefault(); submit(); }}>
  <input value={identifier} onChange={(e) => setIdentifier(e.target.value)} />
  <button disabled={isSubmitting}>Continue</button>
</form>

Options:

PropertyTypeRequiredDescription
flowType'signin' | 'signup' | 'account_manage'YesPassed to start()

Returns: { identifier, setIdentifier, isSubmitting, submit }. submit() trims the identifier, ignores empty input, and calls start(identifier, flowType).


useOtpForm()

State for the code input on enter_code, enter_totp, and setup_totp steps. It detects whether the current step wants an OTP or a TOTP code (including after a retryable TOTP error) and routes submit() to submitCode() or submitTotp() accordingly — one form handles both.

const { code, setCode, submit, resend, destination, isTotp, isSubmitting } =
  useOtpForm();

<p>{isTotp ? 'Enter your authenticator code' : `Code sent to ${destination}`}</p>

Returns:

PropertyTypeDescription
codestringCurrent input value
setCode(code: string) => voidUpdate the input
submit() => Promise<void>Submit as OTP or TOTP based on the current step. Clears the input once the submission resolves — including on a wrong code, since SDK actions resolve to an error state rather than throw
resend() => Promise<void>Resend the OTP (valid on enter_code)
destinationstring | nullMasked email/phone from the enter_code step
isTotpbooleanTrue when the current step expects an authenticator code
isSubmittingbooleanTrue while a submit is in flight

usePhoneForm()

State for the phone number input on the setup_sms step.

const { phone, setPhone, submit, isSubmitting } = usePhoneForm();

Returns: { phone, setPhone, submit, isSubmitting }. submit() trims the value, ignores empty input, calls submitPhone(phone) (E.164 format), and clears the input once the submission resolves (on error states too — SDK actions resolve rather than throw).


useMethodSelection(state)

Extract the available methods from a select_method step.

import type { AuthStateStep } from '@hawcx/react';

function MethodPicker({ state }: { state: AuthStateStep }) {
  const { methods } = useMethodSelection(state);
  // methods: Method[] — empty if the current step is not select_method
  return methods.map((m) => <MethodButton key={m.name} method={m} />);
}
ParameterTypeDescription
stateAuthStateStepThe current step state

Returns: { methods: Method[], session: string } — memoized; methods is [] when the current step is not select_method.


useMethodDisplayName(method)

Display name for a method: its label, falling back to name. Memoized.

const displayName = useMethodDisplayName(method);

Backend API

HawcxOAuth

Exchange authorization codes for verified user claims.

import { HawcxOAuth } from '@hawcx/oauth-client';

// Discovery mode (recommended): enforces iss + aud. Construct once at startup.
const oauth = await HawcxOAuth.fromIssuer({
  issuer: process.env.HAWCX_BASE_URL!,     // your environment's Base URL
  configId: process.env.HAWCX_CONFIG_ID!,  // sent as the X-Config-Id header
  clientId: process.env.HAWCX_CLIENT_ID!,  // expected `aud` on id_tokens
});

fromIssuer config:

PropertyTypeRequiredDescription
issuerstringYesYour environment's Base URL; discovery target and expected iss
configIdstringYesConfig ID — sent as the X-Config-Id header
clientIdstringYesClient ID — enforced as the aud claim on id_tokens
timeoutnumberNoPer-request timeout (ms) for token exchange + JWKS fetch (default 10000)
discoveryTimeoutnumberNoTimeout (ms) for the one-time discovery fetch (default 5000)
clockToleranceSecondsnumberNoTolerated clock skew (s) when verifying exp / nbf (default 10)

For 4.x back-compat the legacy single-arg constructor new HawcxOAuth({ configId, baseUrl }) (signature-only — no iss/aud enforcement) is still supported; see the Node.js API reference for its options.

from hawcx_oauth_client import HawcxOAuth
import os

oauth = HawcxOAuth.from_issuer(
    os.environ['HAWCX_BASE_URL'],   # issuer URL (Base URL from Admin Console)
    os.environ['HAWCX_CONFIG_ID'],
    os.environ['HAWCX_CLIENT_ID'],  # expected `aud` on issued id_tokens
)

exchangeCode(authCode, codeVerifier)

Exchange authorization code for verified claims.

const { idToken, claims } = await oauth.exchangeCode(authCode, codeVerifier);

// claims.sub = user ID
// claims.email = verified email
result = oauth.exchange_code(auth_code, code_verifier)

# result.claims['sub'] = user ID
# result.claims['email'] = verified email

Returns:

PropertyTypeDescription
idTokenstringRaw JWT (don't use as access token)
claimsobjectVerified claims (sub, email, etc.)

Error Types

import { TokenExchangeError, TokenVerificationError } from '@hawcx/oauth-client';

try {
  const { claims } = await oauth.exchangeCode(authCode, codeVerifier);
} catch (error) {
  if (error instanceof TokenExchangeError) {
    // Code exchange failed (invalid/expired code)
    console.error(error.message, error.statusCode);
  }
  if (error instanceof TokenVerificationError) {
    // JWT verification failed
    console.error(error.message);
  }
}
ErrorWhenRecovery
TokenExchangeErrorCode invalid, expired, or already usedAsk user to re-authenticate
TokenVerificationErrorJWT signature/claims failedLog incident, re-authenticate

Flow Types

Most apps can omit the flow type — start(email) defaults to 'signin', and tenants configured with unknown_user='create' transition new users into signup server-side. On stricter tenant policies, pass 'signup' explicitly for registration.

TypeWhen to Use
signinReturning user login
signupNew user registration
account_manageStep-up auth for sensitive actions

Last updated on