Client integration / Web SDK

Client integration

Use the official web components for binding and confirmation. Find component options, events and website backend examples here.

The AuthLink production API is https://api.getauthlink.com. Use your own API for the application backend; the AuthLink website domain is not an API endpoint.

JavaScript quickstart

Use @auth-link/web to call your application backend. Same-origin is the default; explicitly allowed cross-origin backends are also supported. Keep tenant API keys on the backend.

View the npm package ↗

npm install @auth-link/web

Import from @auth-link/web, @auth-link/web/react, @auth-link/web/vue and @auth-link/web/styles.css. Import CSS explicitly. Supports React 18.2+ / 19, Vue 3.3+, ESM and TypeScript; tooling requires Node 22.13+.

Same-origin cookie / session setup

Your backend generates and stores the CSRF token for the current login session using your framework or session endpoint. csrfToken below is a server-template variable and must be escaped when rendered. Do not replace it with a browser-generated value or a tenant API key.

<meta name="csrf-token" content="{{ csrfToken }}">
import { createAuthLinkClient } from '@auth-link/web';
import '@auth-link/web/styles.css';

// Read the token rendered by YOUR backend for the current login session.
// This helper is application code, not an SDK export.
export function readCsrfToken(): string {
  const token = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content;
  if (!token) throw new Error('Missing session CSRF token');
  return token;
}

export const client = createAuthLinkClient({
  baseUrl: '/api/authlink',
  csrfToken: readCsrfToken,
  authLinkOrigin: 'https://api.getauthlink.com',
});

readCsrfToken is an application helper in this example, not an SDK export. Both creation and result endpoints must validate the CSRF header against the login session. Sending the header alone is insufficient. Refresh the token after session changes and never publicly cache pages containing session tokens.

Bind or confirm from a button handler

import { openAuthLinkBind, openAuthLinkConfirm, requestKey } from '@auth-link/web';
import type { AuthLinkModal, Mode } from '@auth-link/web';
import { client } from './authlink-client';

let modal: AuthLinkModal | undefined;
let creating = false;
let createKey: string | undefined;
let creation: AbortController | undefined;

// Call startAuthLink('bind') or startAuthLink('confirm') from a button handler.
export async function startAuthLink(mode: Mode) {
  if (creating || modal) return;
  creating = true;
  creation = new AbortController();
  try {
    createKey ??= requestKey(); // Retain this key if creation fails.
    const ticket = await client.create(mode, {
      idempotencyKey: createKey, signal: creation.signal,
    });
    createKey = undefined;
    const open = mode === 'bind' ? openAuthLinkBind : openAuthLinkConfirm;
    modal = open({
      ticket, client, locale: 'zh-CN', variant: 'A',
      onConfirmed: result => {
        modal = undefined;
        console.log('Confirmed in UI:', result.requestId);
      },
      onRejected: result => { modal = undefined; console.log(result.code); },
      onExpired: () => { modal?.destroy(); modal = undefined; },
      onClose: () => { modal = undefined; },
      onError: error => console.error(error.message),
    });
  } finally { creating = false; }
}

// Call on page/component teardown. Business authorization stays on the backend.
export function destroyAuthLink() { creation?.abort(); modal?.destroy(); modal = undefined; }
Create requests only from user actions, never at module load, in a React effect, Vue watch or SSR. Reuse the creation key after a failed call; abort creation and destroy the dialog on teardown. onConfirmed updates the UI; the backend authorizes business actions.

Separate frontend and backend origins

baseUrl targets your application backend, not the AuthLink tenant API. For cross-origin calls use an absolute baseUrl and exact allowedBackendOrigins. Origins contain only scheme, host and port, with no paths, wildcards or user input. Use HTTPS in production; HTTP loopback is allowed for local development. authLinkOrigin separately checks the AuthLink service origin in QR data.

Cross-origin cookie / session

GET /api/session is an example endpoint implemented by your application. Return csrfToken for the authenticated session with Cache-Control: no-store and appropriate CORS. Replace it with your existing session endpoint.

import { createAuthLinkClient } from '@auth-link/web';

/** /api/session is an example endpoint implemented by your application backend. */
export async function createSessionBackendClient(backendOrigin: string) {
  const response = await fetch(`${backendOrigin}/api/session`, {
    credentials: 'include', cache: 'no-store', redirect: 'error',
  });
  if (!response.ok) throw new Error('Session initialization failed');
  const session: unknown = await response.json();
  if (!session || typeof session !== 'object' || !('csrfToken' in session) ||
      typeof session.csrfToken !== 'string' || !session.csrfToken) {
    throw new Error('The session endpoint must return { csrfToken: string }');
  }
  let currentCsrfToken = session.csrfToken;
  return {
    client: createAuthLinkClient({
      baseUrl: `${backendOrigin}/api/authlink`,
      allowedBackendOrigins: [backendOrigin],
      credentials: 'include',
      csrfToken: () => currentCsrfToken,
    }),
    // Call after your application's session/token refresh; recreate on account changes.
    updateCsrfToken(value: string) { currentCsrfToken = value; },
  };
}

Call createSessionBackendClient after login and pass the returned client to components. Call updateCsrfToken after session refresh. Cross-origin cookies require credentials: include; SameSite, Secure, domain/path and browser restrictions still apply. Close old dialogs and recreate state on logout or account changes.

Cross-origin Bearer / JWT

import { createAuthLinkClient } from '@auth-link/web';

/** auth is your website's existing login state, updated by its login/refresh flow. */
export function createBearerBackendClient(backendOrigin: string, auth: { accessToken: string }) {
  return createAuthLinkClient({
    baseUrl: `${backendOrigin}/api/authlink`,
    allowedBackendOrigins: [backendOrigin],
    credentials: 'omit',
    accessToken: () => auth.accessToken,
  });
}

auth is your website’s existing login state. Its login or refresh flow updates auth.accessToken; the callback reads the latest value for every creation and poll. Use a website user login token, never an AuthLink tenant API key. Omit CSRF only when credentials is omit and your backend authenticates exclusively with Bearer tokens, with no cookie fallback.

Application backend CORS

Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-CSRF-Token, Idempotency-Key, Authorization
Vary: Origin

Your application backend must handle OPTIONS preflight and send CORS headers on session, actual and error responses. Do not use a wildcard origin with cookies. Validate against your backend origin allowlist before returning headers, and include any custom headers. Authenticate actual requests and verify CSRF and ownership. The SDK cannot enable backend CORS or read responses with no-cors.

Error codes

Error codesHandling
BACKEND_ORIGIN_NOT_ALLOWEDThe application backend origin is not allowed. Check allowedBackendOrigins.
INVALID_BACKEND_ORIGIN / INVALID_BASE_URLCheck that baseUrl is a valid application API prefix. Origins in allowedBackendOrigins cannot contain paths or wildcards.
CSRF_REQUIRED / ACCESS_TOKEN_REQUIREDA login or CSRF token is missing; obtain a valid session.
RESERVED_HEADERDo not override SDK-managed authentication or request headers with headers.

Client and dialog options

createAuthLinkClient optionsType / defaultDescription
baseUrlstring / /api/authlinkApplication backend prefix; use a full URL for cross-origin calls, not the AuthLink API domain.
allowedBackendOriginsreadonly string[] / []additional exact application backend origins. Same-origin remains allowed.
credentialsRequestCredentialsdefaults to same-origin, or omit with accessToken. Use include for cross-origin cookies.
csrfToken() => string | Promise<string>Reads the current session token for each request. Required for cookie or mixed authentication.
csrfHeaderNamestring / X-CSRF-Tokenmatch the CSRF header validated by your backend framework.
accessToken() => string | Promise<string>reads the website user login token per request, without the Bearer prefix. Never supply a tenant key.
headersHeadersInit | (() => HeadersInit | Promise<HeadersInit>)additional application headers or a provider. Cannot override Accept, Content-Type, Authorization, Idempotency-Key or the CSRF header.
authLinkOriginstring / optionalChecks the AuthLink service origin in QR data; configure separately from allowed backend origins.
fetchtypeof fetch / globalThis.fetchOptional custom transport; client destination validation still applies.
Dialog / component options Type / default Description
ticket / client Ticket / AuthLinkClient, required Backend-created request and application result client
locale Locale / zh-CN 13 UI languages; unsupported values fall back to Simplified Chinese
variant A / B / C, default A A: minimal card; B: split layout; C: compact card / mobile bottom sheet
showQr boolean / false Whether confirmation also shows a QR. Binding always shows one.
intervalMs number / 1500 Polling interval in ms, 250–10000; does not extend expiry
onRefresh () => void / optional After expiry, your website creates a new request and updates ticket
onConfirmed / onRejected (result) => void or Promise<void> Closes automatically on confirmation / rejection; does not trigger user-close events
onExpired / onError () => void;(Error) => void Expiry, polling or validation errors
onClose () => void Clear ticket when the user closes; destroy() does not trigger this callback

Supported languages:zh-CN, zh-TW, en, ja, bn, es, fil, hi, id, ms, pt, th, vi. Cancel / close only stops frontend observation, without rejecting the server request. Language and layout changes do not recreate requests.

React and Vue

Reuse authlink-client.ts above. Both examples include buttons, request creation, result messages and dialog cleanup. Use mode bind for binding and confirm for phone confirmation.

Complete React example

"use client";
import { useRef, useState } from 'react';
import { requestKey } from '@auth-link/web';
import type { Mode, Ticket } from '@auth-link/web';
import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/react';
import { client } from './authlink-client';

export function AuthLinkPanel({ mode }: { mode: Mode }) {
  const [ticket, setTicket] = useState<Ticket | null>(null);
  const [busy, setBusy] = useState(false);
  const [message, setMessage] = useState('');
  const key = useRef<string | null>(null);
  const creating = useRef(false);
  function finish(message: string) {
    setTicket(null);
    setMessage(message);
  }
  function showBound(requestId: string) {
    // UI feedback only. The backend has already verified and saved the binding.
    finish(`Phone bound (request ${requestId})`);
  }
  function showConfirmed(requestId: string) {
    finish(`Confirmed (request ${requestId})`);
  }
  const start = async () => {
    if (creating.current || ticket) return;
    creating.current = true;
    setBusy(true);
    setMessage('');
    try {
      key.current ??= requestKey();
      setTicket(await client.create(mode, { idempotencyKey: key.current }));
      key.current = null;
    } catch { setMessage('Request failed; retry the same creation.'); }
    finally { creating.current = false; setBusy(false); }
  };
  const Component = mode === 'bind' ? AuthLinkBind : AuthLinkConfirm;
  return <>
    {!ticket && <button type="button" disabled={busy} onClick={() => void start()}>Start verification</button>}
    {ticket && <Component ticket={ticket} client={client}
      onConfirmed={result => result.purpose === 'bind' ? showBound(result.requestId) : showConfirmed(result.requestId)}
      onClose={() => finish('Closed')}
      onRejected={() => finish('Request rejected on the phone')}
      onExpired={() => finish('Request expired; start again')}
      onError={error => setMessage(error.message)} />}
    <p role="status">{message}</p>
  </>;
}

Complete Vue example

<script setup lang="ts">
import { ref, shallowRef } from 'vue';
import { requestKey } from '@auth-link/web';
import type { Confirmation, Mode, Ticket } from '@auth-link/web';
import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/vue';
import { client } from './authlink-client';
const props = defineProps<{ mode: Mode }>();
const ticket = shallowRef<Ticket | null>(null);
const busy = ref(false);
const message = ref('');
let key: string | null = null;
function finish(text: string) { ticket.value = null; message.value = text; }
function confirmed(result: Confirmation) {
  finish(`${result.purpose === 'bind' ? 'Bound' : 'Confirmed'} (request ${result.requestId})`);
}
async function start() {
  if (busy.value || ticket.value) return;
  busy.value = true;
  key ??= requestKey();
  try {
    ticket.value = await client.create(props.mode, { idempotencyKey: key });
    key = null;
  } catch { message.value = 'Request failed; retry the same creation.'; }
  finally { busy.value = false; }
}
</script>

<template>
  <button v-if="!ticket" type="button" :disabled="busy" @click="start">Start verification</button>
  <AuthLinkBind v-else-if="mode === 'bind'" :ticket="ticket" :client="client"
    @confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected on the phone')"
    @expired="finish('Request expired; start again')" @error="message = 'Network error; retry this request.'" />
  <AuthLinkConfirm v-else :ticket="ticket" :client="client"
    @confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected on the phone')"
    @expired="finish('Request expired; start again')" @error="message = 'Network error; retry this request.'" />
  <p role="status">{{ message }}</p>
</template>

The backend verifies, consumes and saves binding or confirmation before returning confirmed. showBound and finish only update the UI and clear the ticket. After a reload, fetch actual binding state from your backend. Component state and localStorage never authorize business actions.

Retain the creation key after failure to retry the same request. Clear the ticket after success, rejection, expiry or close; keep the dialog on network errors for retry. Keep client and ticket stable. Unmounting stops polling. useAuthLink supports custom interfaces; Vue exposes snapshot as a shallow ref.

Application backend contract and types

Implement these endpoints in your application. Authenticate the session or website Bearer token, require CSRF with cookies, and verify request ownership and expiry. Never expose an anonymous proxy.

Website-backend endpoint Input Responsibility
POST /api/authlink/requests body {"purpose":"bind"} or confirm;Idempotency-Key; session + CSRF or website Bearer token Get the user from the session, persist request context, call the platform's create API and return the public ticket.
POST /api/authlink/requests/{id}/result body {}; session + CSRF or website Bearer token Check ownership and query AuthLink. After approved / consumed, idempotently consume, validate and persist, then return confirmed.

type Ticket = {
  requestId: string; purpose: 'bind' | 'confirm';
  qr: string; pairingCode: string; expiresAt: string;
  displayCode?: {
    value: string; serverTime: string; validUntil: string;
    periodSeconds: 30;
  };
};
// Pending: { requestId, status: 'new' | 'pending' | 'approved' |
//   'consumed' | 'rejected' | 'expired', displayCode? }
// Successful result after backend validation and persistence (minimum fields):
type Confirmation = {
  status: 'confirmed'; requestId: string; verificationId: string;
  purpose: 'bind' | 'confirm';
  authenticationPolicy: string; expiresAt: string;
};
// Rejection event: { status:'rejected', code:'REJECTED', requestId, purpose }

Return displayCode unchanged and hide expired codes. The official SDK validates the result, which must match the current request and website session. Remounting a component does not replace a saved backend confirmation.

Lifecycle and UI rules

  • Codes change every 30 seconds; requests last up to 120 seconds. The code is not a standalone TOTP sign-in password.
  • Clean up observers on cancellation / unmount. Confirmation and rejection close automatically; your website clears ticket.
  • Closing the frontend does not revoke the request; the original deadline still applies. Do not repeatedly create requests to recover uncertain outcomes.
  • Your website CSP must allow img-src 'self' data:. QR codes are generated locally in the browser, never sent to an external image service.
  • App deletion requires explicit confirmation of permanent revocation. The website account cannot bind again. Update in place without uninstalling or clearing app data.