AuthenSeeDocs

Embed (popup drop-in)

Launch the co-branded hosted flow in a popup with @rebellion-systems/authensee-embed — enrollment, authentication, and recovery without leaving your page.

What you'll build: a button on your page that opens the AuthenSee hosted flow in a popup, receives the result back on your page without a full-page navigation, and completes the login on your backend.

When to use this: any third-party web app. The popup is the recommended surface because the user never leaves your page, and because a popup is a top-level browsing context — the only place the WebAuthn passkey ceremony works across browsers. navigator.credentials.create() (registration) and discoverable get() are blocked or restricted inside cross-origin iframes, and the hosted flow refuses framing outright. If you'd rather hand off the whole page, use the redirect flow instead.

How it works

Your page                Your backend            AuthenSee popup        Auth server
  |                          |                          |                    |
  |  1. open() (on click)    |                          |                    |
  |     pops a blank window  |                          |                    |
  |                          |                          |                    |
  |  2. flowUrl() mints a session                       |                    |
  |  ----------------------> | POST /v1/sessions        |                    |
  |                          | (x-api-key: sk_...)  ----|------------------> |
  |                          | <-- { hostedUrl, ... } --|------------------- |
  |  <-- hostedUrl ----------|                          |                    |
  |                          |                          |                    |
  |  3. popup navigates to hostedUrl  ---------------->  |                    |
  |                          |        user enrolls / authenticates (popup)   |
  |                          |                          | --- ZK proof ----> |
  |                          |                          |                    |
  |  4. popup lands on your callbackUrl?authResultCode=…|                    |
  |     callback page calls relayCallback()             |                    |
  |  <-- BroadcastChannel("authensee") -----------------|                    |
  |                          |                          |                    |
  |  5. onComplete({ authResultCode }) fires; popup closes                   |
  |                          |                          |                    |
  |  6. exchange authResultCode server-side             |                    |
  |  ----------------------> | POST /v1/auth-results/exchange (sk_...)      |

Install

npm install @rebellion-systems/authensee-embed

Or load the IIFE/CDN build, which exposes a global window.AuthenSee:

<script src="https://cdn.authensee.com/v1/authensee.js"></script>
<script>
  AuthenSee.open({ flowUrl, onComplete });
</script>

The package ships ESM, CJS, and IIFE builds.

Mint a session on your backend

Your backend creates the session with its secret key and passes the callbackUrl (your callback page) so the popup knows where to land:

// POST /api/authensee/session (your backend)
app.post('/api/authensee/session', async (req, res) => {
  const r = await fetch('https://api.authensee.com/v1/sessions', {
    method: 'POST',
    headers: {
      'x-api-key': process.env.AUTHENSEE_SECRET_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      scope: 'full',
      externalUserId: req.user.id,
      callbackUrl: 'https://app.example.com/authensee/callback',
    }),
  });
  const session = await r.json();
  // session.hostedUrl → https://auth.authensee.com/flow/{flowCode}
  res.json({ hostedUrl: session.hostedUrl });
});

POST /v1/sessions returns a single-use flowCode inside the ready-to-use hostedUrl — see the sessions reference for the full request and response. Return only hostedUrl to the browser, never sessionToken.

Open the popup from a click handler

Call open() inside a user gesture so the browser doesn't block the popup. Pass flowUrl as a function: the popup opens immediately (gesture-safe) and navigates once your mint resolves.

import { open } from '@rebellion-systems/authensee-embed';
 
button.addEventListener('click', () => {
  open({
    flowUrl: async () => {
      const res = await fetch('/api/authensee/session', { method: 'POST' });
      const { hostedUrl } = await res.json();
      return hostedUrl;
    },
    onComplete: ({ authResultCode, linked, providerSubject, sessionId }) => {
      if (authResultCode) {
        // Normal login/enrollment completion — exchange server-side.
        completeLoginOnBackend(authResultCode);
      } else if (linked) {
        // The user linked an existing persona instead of enrolling.
        recordLink(providerSubject);
      }
    },
    onError: ({ code, message }) => {
      // code is "POPUP_BLOCKED" when the browser blocked the popup.
      console.error(code, message);
    },
  });
});

Relay the result from your callback page

On the page you registered as callbackUrl, call relayCallback(). It reads the one-time result from the URL, publishes it to the opener over a same-origin BroadcastChannel named "authensee", and closes the popup.

// https://app.example.com/authensee/callback
import { relayCallback } from '@rebellion-systems/authensee-embed';
 
relayCallback();

The result fields delivered to onComplete:

FieldTypeDescription
authResultCodestring | nullOne-time result code — exchange it server-side with your secret key. null for enroll-only flows and for persona-link completions.
linkedstring | nullPresent ("1") when an existing persona was linked rather than newly enrolled
providerSubjectstring | nullYour provider-scoped stable alias for the persona, when applicable
sessionIdstringThe AuthenSee session ID this flow ran under

Exchange the result code on your backend

import { createAuthenSeeSdk } from '@rebellion-systems/authensee-sdk';
 
const authensee = createAuthenSeeSdk({
  serverUrl: 'https://api.authensee.com',
  apiKey: process.env.AUTHENSEE_SECRET_KEY!,
});
 
const result = await authensee.exchangeAuthResult(authResultCode);
// result.providerSubject — your stable user identity
// result.token          — EdDSA-signed JWT (verify against the JWKS)

The code is single-use and short-lived; exchange it as soon as your backend receives it. See auth results for the full response and JWT claims.

open() options

OptionTypeRequiredDescription
flowUrlstring | (() => string | Promise<string>)YesThe hosted-flow URL, or a function returning it (e.g. an async mint). A function is opened gesture-safely.
onComplete(result) => voidYesCalled when the flow completes successfully
onError(error) => voidNoCalled on error or when the popup is blocked (code: "POPUP_BLOCKED")
featuresstringNoOverride the popup window features (size, chrome)

open() returns a handle with close() and focus() methods.

No iframe embedding

The hosted flow cannot be embedded in an iframe: it sends Content-Security-Policy: frame-ancestors 'none' on every route, so any iframe pointed at it is refused by the browser. The popup (open()) or a full-page redirect are the only supported integration surfaces.

Branding

The hosted flow is co-branded from your provider configuration: AuthenSee owns the frame and you contribute a logo, display name, one brand color, one copy line, and a light or dark surface. See theming.

Troubleshooting

The popup never opens (POPUP_BLOCKED). open() must be called synchronously inside a user gesture (a click handler). If you await anything before calling open(), browsers treat the popup as unsolicited — pass your async mint as the flowUrl function instead, and let open() run first.

Session creation fails with VALIDATION_ERROR: callbackUrl origin ... is not on this provider's allowlist. The origin of your callbackUrl (scheme + host + port, exactly) must be listed in your provider's allowed callback origins. An empty allowlist rejects every callback URL — add your origin in the dashboard before testing.

The user refreshed the popup and got "flow expired". The flowCode in the URL is single-use: it's redeemed for an in-memory token when the flow loads, and a full-page refresh can't redeem it again. Mint a fresh session and reopen the flow.

onComplete never fires. Confirm your callback page actually calls relayCallback(), and that the callback page is served from the same origin as the opener — the BroadcastChannel relay is same-origin by design.

The user got an "update your security setup" step they didn't ask for. Your factor-combination policy changed since the user enrolled, and their login hit 409 policy_upgrade_required. The hosted flow handles the upgrade ceremony automatically — the user proves their existing factor once and adds the new one. See the upgrade ceremony.

Exchanging the code fails with VALIDATION_ERROR. The code was already consumed (they're strictly single-use) or expired. Treat this as a failed login and start a fresh flow — never retry an exchange with the same code.

Next

On this page