Helix.ID
Sign In

Quickstart · 5 min

v0.2.1 · Updated 17 Sep 2026View as Markdown

Verify your first user.

Four steps from a signed redirect URL to a verified callback. The redirect flow needs nothing but the server SDK.

1. Generate the redirect URL (backend)

Call createAuthUrl() on your backend to generate a unique, signed URL and a cryptographic nonce. Store the nonce in the user's session before redirecting.

createAuthUrl HMAC-SHA256-signs the URL params with your shared secret and appends the signature as a sig query param, so it needs the secret option. It is async — it uses the Web Crypto API, so it returns a Promise and must be awaited.

js
import { createAuthUrl } from "@helix-id/node";

app.get("/auth/start", async (req, res) => {
  const state = Buffer.from(JSON.stringify({ action: "approve_tx", txId: "abc123", returnTo: "/dashboard" })).toString(
    "base64",
  );

  const { url, nonce } = await createAuthUrl({
    spaceId: process.env.HELIX_SPACE_ID,
    state,
    // Required. The browser origin your callback is served from —
    // Helix targets its postMessage at exactly this origin.
    origin: process.env.APP_ORIGIN,
    // Required. Signs the URL params; the signature rides along as `sig`.
    secret: process.env.HELIX_SECRET,
  });

  req.session.helixNonce = nonce;
  // Return only `url` — it already carries `sig`. Never send the secret to the browser.
  res.json({ url });
});

The result also carries the raw signature as sig if you need it separately; the url already includes it. Do not append, reorder or rewrite query params after signing — any edit invalidates the signature.

2. Redirect the user (frontend)

Fetch the URL from your backend and redirect the user's browser to the Helix portal.

js
const { url } = await fetch("/auth/start").then((r) => r.json());
window.location.href = url;

3. Verify the callback (backend)

After the user completes the voice challenge, Helix redirects to your callback endpoint. Call verifyCallback() to validate the HMAC signature, nonce, and timestamp.

js
import {
  verifyCallback,
  InvalidSignatureError,
  NonceMismatchError,
  ExpiredTimestampError,
  MalformedCallbackError,
} from "@helix-id/node";

app.get("/auth/callback", async (req, res) => {
  // verifyCallback expects every field to be a plain string.
  // Express may parse duplicate query params as arrays —
  // normalize req.query before passing it in.
  const query = Object.fromEntries(Object.entries(req.query).map(([k, v]) => [k, Array.isArray(v) ? v[0] : String(v)]));

  try {
    const result = await verifyCallback(query, {
      nonce: req.session.helixNonce,
      secret: process.env.HELIX_SECRET,
    });

    req.session.helixNonce = null;
    const state = JSON.parse(Buffer.from(result.state, "base64").toString());

    if (result.status === "verified") {
      res.redirect(state.returnTo);
    } else {
      res.redirect(`${state.returnTo}?error=${result.statusCode}`);
    }
  } catch (error) {
    // every Helix error carries a stable .code, e.g. INVALID_SIGNATURE
    if (error instanceof InvalidSignatureError) return res.status(401).json({ error: error.code });
    if (error instanceof NonceMismatchError) return res.status(401).json({ error: error.code });
    if (error instanceof ExpiredTimestampError) return res.status(401).json({ error: error.code });
    if (error instanceof MalformedCallbackError) return res.status(400).json({ error: error.code });
    throw error;
  }
});

4. Store & reuse the Enrollment ID (backend)

If your Space has Voice MFA or Banned List enabled, verifyCallback returns an enrollmentId. Store it against the user — it is how Helix recognizes them on their next voice-auth session instead of enrolling a duplicate.

js
const result = await verifyCallback(query, { nonce: req.session.helixNonce, secret: process.env.HELIX_SECRET });

if (result.enrollmentId) {
  await db.users.update(userId, { helixEnrollmentId: result.enrollmentId });
}

Pass it back into createAuthUrl the next time that same user starts a voice-auth session:

js
const { url, nonce } = await createAuthUrl({
  spaceId: process.env.HELIX_SPACE_ID,
  state,
  origin: process.env.APP_ORIGIN,
  secret: process.env.HELIX_SECRET,
  // Omit on a user's first session — Helix enrolls them and returns a fresh Enrollment ID.
  enrollmentId: user.helixEnrollmentId,
});