Helix.ID
Sign In

Quickstart · 7 min

v0.3.0 · Updated 12 Oct 2026View as Markdown

Verify your first user.

Signed redirects, verified callbacks, Enrollment IDs, and Reference List management using 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 Reference 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,
});

5. Handle a Reference List match (backend)

If your Space has Reference List enabled, a new enrollment comes back with result.referenceList. A match does not block the user — the enrollment still completes — so act on it yourself, using the category you added the person with. denied and vip below are recommended client-defined labels with no built-in behavior; custom labels work too. Your server adds people using addToReferenceList.

js
if (result.referenceList?.result === "CONFIRMED_MATCH") {
  const { matchedEnrollmentId, matchedCategory } = result.referenceList;
  if (matchedCategory === "denied") await reviewQueue.add({ userId, matchedEnrollmentId });
  if (matchedCategory === "vip") await vipDesk.notify({ userId, matchedEnrollmentId });
}

Treat UNSCOREABLE, and any value you do not recognize, as "not screened" rather than "clear". All values are listed under Reference List results.

6. Add or remove a person (backend)

Use the Enrollment ID stored in step 4 when your policy calls for listing a person. Adding requires an active server-side voice template; an ID alone does not guarantee eligibility. Add the person in one server action:

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

const options = {
  spaceId: process.env.HELIX_SPACE_ID,
  secret: process.env.HELIX_SECRET,
  enrollmentId: user.helixEnrollmentId,
};

await addToReferenceList({ ...options, category: "denied", reason: "manual_review" });

After your review upholds an appeal, remove them in a separate server action:

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

await removeFromReferenceList({
  spaceId: process.env.HELIX_SPACE_ID,
  secret: process.env.HELIX_SECRET,
  enrollmentId: user.helixEnrollmentId,
  reason: "APPEAL_UPHELD",
});

Keep these calls on your server. The helpers sign POST /api/v1/reference-list for the selected Space/environment and throw ReferenceListError on HTTP failures. See Reference List management for every option, category convention, and HTTP status.