API Docs Security Widget Guide Admin Guide otp.royaltycoding.com
Widget Guide · v1.6

OTP Web Components

Drop-in TOTP components — no build step, no framework required. Just include the script tag and use the custom HTML elements anywhere on your page.

Web Components Zero Dependencies Shadow DOM Error Banners Icon Override Dark & Light

⚡ Installation

1
Include the widget script
Add this <script> tag before your closing </body> tag — or in <head> with defer.
html
<script src="https://otp.royaltycoding.com/otp-widget.js"></script>
2
Add the custom elements to your HTML
Place any of the three components wherever you need them. All attributes are optional — the components degrade gracefully.
html
<!-- On your account settings page -->
<otp-setup-btn
  email="user@example.com"
  api="https://otp.royaltycoding.com"
  api-token="your-token-here"
  theme="dark">
</otp-setup-btn>

<!-- On your login page -->
<otp-verify-btn
  email="user@example.com"
  api="https://otp.royaltycoding.com">
</otp-verify-btn>

<!-- On your security settings page -->
<otp-disable-btn
  email="user@example.com"
  api="https://otp.royaltycoding.com">
</otp-disable-btn>
3
Listen for events (optional)
React to success and errors using standard DOM event listeners. Events bubble up through the DOM, so you can listen at document level.
javascript
document.addEventListener('otp-setup-complete', (e) => {
  console.log('2FA enabled for:', e.detail.email);
  // Reload user session, show success toast, etc.
});

document.addEventListener('otp-verified', (e) => {
  console.log('Verified:', e.detail.email);
  // Redirect to protected page
  window.location.href = '/dashboard';
});

document.addEventListener('otp-disabled', (e) => {
  console.log('2FA removed for:', e.detail.email);
});

document.addEventListener('otp-error', (e) => {
  const { message, httpStatus, errorType } = e.detail;
  // errorType: "rate" | "cors" | "apierr" | "network"
  console.error('[otp-error]', errorType, httpStatus, message);
});
4
Add your domain to the allowed origins
Your domain must be in the allowed_origins table or all API calls will return 403. See the Security Guide for details.
sql
INSERT INTO allowed_origins (origin) VALUES ('https://yourdomain.com');

🧩 Components Overview

otp-setup-btnSetup
3-step flow: email → QR code → confirm code Fires: otp-setup-complete
otp-verify-btnVerify
2-step flow: email (optional) → enter code Fires: otp-verified
otp-disable-btnDisable
2-step flow: email (optional) → confirm with code Fires: otp-disabled

📋 All Attributes

All three components share the same attribute interface. All attributes are optional — sensible defaults are applied for each.

AttributeTypeDefaultComponentsDescription
email string "" all Pre-fills the email input and skips the email step entirely when provided. The user jumps straight to the QR / code entry step. Pass this from your server after login.
api string https://otp.royaltycoding.com all Base URL for the OTP API. Override this if you self-host the backend on a different domain or port.
theme "dark" | "light" "dark" all Controls the modal overlay appearance. "dark" = dark background with light text. "light" = white background with dark text.
label string component default all Overrides the button text. Defaults: otp-setup-btn → "Enable 2FA", otp-verify-btn → "Verify 2FA Code", otp-disable-btn → "Disable 2FA".
icon string | "" component default all Overrides the button icon. Any emoji, SVG string, or text. Set icon="" (empty string) to hide the icon completely. Omitting the attribute uses the default icon. Live-reactive: updating the attribute re-renders the button.
api-token string "" all Required. Bearer token for API authentication. Sent as Authorization: Bearer <token> on every request. Create tokens via /admin/tokens. If missing or invalid the widget shows a 401 error banner. Never hard-code this in public HTML — set it dynamically from your server-rendered page.
Live attributes: The label and icon attributes are observed — changing them via JavaScript (el.setAttribute('icon', '🛡️')) will immediately re-render the button without needing a full page refresh.

📡 Events

All events bubble (bubbles: true) and are composed (composed: true), so they cross shadow DOM boundaries and can be caught at any ancestor level.

EventFired bydetail payloadWhen
otp-setup-complete setup { email, secret } TOTP was successfully configured and enabled on the account.
otp-verified verify { email } A valid 6-digit code was accepted by the API at login.
otp-disabled disable { email } TOTP was successfully removed from the account.
otp-error all { message, httpStatus, errorType } Any API or network error occurred. errorType: "rate" | "cors" | "apierr" | "network".

🔐 Setup Button — Live Demos

otp-setup-btn

Opens a 3-step modal: enter email → scan QR code → enter confirmation code. On success, TOTP is enabled for that (email, origin) pair.

Default (dark theme, no email pre-fill)
Demo
html
<otp-setup-btn
  api="https://otp.royaltycoding.com">
</otp-setup-btn>
With pre-filled email (skips email step)
Demo — goes straight to QR step
html
<otp-setup-btn
  email="alice@example.com"
  api="https://otp.royaltycoding.com">
</otp-setup-btn>
Light theme
Demo
html
<otp-setup-btn
  api="https://otp.royaltycoding.com"
  theme="light">
</otp-setup-btn>

🛡️ Verify Button — Live Demos

otp-verify-btn

Opens a 2-step modal for login verification. When email is provided it skips straight to the code-entry step (typical login use case).

Default (with email step)
Demo
html
<otp-verify-btn
  api="https://otp.royaltycoding.com">
</otp-verify-btn>
With pre-filled email (skips to code step — typical login flow)
Demo — opens straight to code entry
html
<otp-verify-btn
  email="alice@example.com"
  api="https://otp.royaltycoding.com"
  api-token="your-token-here">
</otp-verify-btn>
Light theme
Demo
html
<otp-verify-btn
  api="https://otp.royaltycoding.com"
  theme="light">
</otp-verify-btn>

🔓 Disable Button — Live Demos

otp-disable-btn

Requires the user to enter a valid current TOTP code before 2FA is removed — prevents unauthorized removal.

Default
Demo
html
<otp-disable-btn
  api="https://otp.royaltycoding.com">
</otp-disable-btn>
With pre-filled email (skips to code step)
Demo
html
<otp-disable-btn
  email="alice@example.com"
  api="https://otp.royaltycoding.com"
  api-token="your-token-here">
</otp-disable-btn>
Light theme
Demo
html
<otp-disable-btn
  api="https://otp.royaltycoding.com"
  theme="light">
</otp-disable-btn>

🌗 Dark & Light Themes

All three components support both theme="dark" (default) and theme="light". The modal overlay, form fields, error banners, and button states all respond to the theme.

dark (default)
theme="light"
html
<!-- Dark modal (default — no attribute needed) -->
<otp-setup-btn api="https://otp.royaltycoding.com"></otp-setup-btn>
<otp-setup-btn api="https://otp.royaltycoding.com" theme="dark"></otp-setup-btn>

<!-- Light modal -->
<otp-setup-btn api="https://otp.royaltycoding.com" theme="light"></otp-setup-btn>

🎨 Icon Variants

v1.4+

Use the icon attribute to replace the default icon with any emoji, text, or SVG. Set icon="" (empty string) to hide the icon entirely. Omitting the attribute keeps the default.

Default icons (no attribute needed)
Default icons: 🔐 · 🛡️ · 🔓
Custom icons
html — custom icons
<otp-setup-btn   icon="🔑" ...></otp-setup-btn>
<otp-verify-btn  icon="✅" ...></otp-verify-btn>
<otp-disable-btn icon="⛔" ...></otp-disable-btn>
No icon — icon=""
html — icon hidden
<!-- Empty string hides icon completely -->
<otp-setup-btn   icon="" ...></otp-setup-btn>
<otp-verify-btn  icon="" ...></otp-verify-btn>
<otp-disable-btn icon="" ...></otp-disable-btn>

📧 Pre-filled Email

When the email attribute is set, the email-entry step is skipped entirely and the modal opens directly at the action step. This is the recommended usage when the user is already logged in.

All three with email pre-filled
email="demo@example.com" — each modal opens at the action step
html
<!-- Typical server-rendered pattern: -->
<otp-setup-btn   email="{{ user.email }}" api="https://otp.royaltycoding.com"></otp-setup-btn>
<otp-verify-btn  email="{{ user.email }}" api="https://otp.royaltycoding.com"></otp-verify-btn>
<otp-disable-btn email="{{ user.email }}" api="https://otp.royaltycoding.com"></otp-disable-btn>
Server-side rendering: Output the user's email into the email attribute from your templating engine (Handlebars, Blade, Jinja, etc.). This avoids the extra email step and provides a seamless UX.

✏️ Custom Labels

Override button text with the label attribute. Combine with icon for fully customised buttons that match your design language.

Custom labels only
html — custom labels
<otp-setup-btn   label="Activate 2FA"  ...></otp-setup-btn>
<otp-verify-btn  label="Enter Code"    ...></otp-verify-btn>
<otp-disable-btn label="Remove 2FA"    ...></otp-disable-btn>
Custom labels + custom icons
html — label + icon
<otp-setup-btn   icon="🔑" label="Set Up Authenticator" ...></otp-setup-btn>
<otp-verify-btn  icon="✔️" label="Confirm Identity"     ...></otp-verify-btn>
<otp-disable-btn icon="🗑️" label="Turn Off 2FA"         ...></otp-disable-btn>

⚠️ Error Banners

v1.4+

The widget automatically shows contextual error banners inside the modal for all API and network failures. There are four error types, each with distinct styling and messaging.

⏱️
Rate Limit Exceeded HTTP 429
Too many requests in a short period.
Security measure. Wait a few minutes before retrying. Repeated rapid attempts may extend the block.
🚫
Access Denied HTTP 403
This origin is not authorised to use this API.
Your domain is not in the allowed origins list. Contact the administrator.
📡
Network Error HTTP 0
Could not reach the server.
Check internet connection. Server may be temporarily unavailable.
⚠️
Request Failed HTTP 4xx/5xx
An unexpected error occurred.
Try again. If the issue persists, contact support with the error message above.
Fatal vs. recoverable: HTTP 429 and 403 errors replace the entire modal body (fatal — modal must be closed). Wrong-code errors (400/401) shake the digit inputs and allow the user to retry without closing.
javascript — handling error events
document.addEventListener('otp-error', (e) => {
  const { message, httpStatus, errorType } = e.detail;

  switch (errorType) {
    case 'rate':    // 429 — rate limited
      showToast('Too many attempts. Please wait.', 'warning');
      break;
    case 'cors':    // 403 — origin not allowed
      showToast('Domain not authorized.', 'error');
      break;
    case 'network': // 0   — no connection
      showToast('Network error. Check connection.', 'error');
      break;
    case 'apierr':  // other 4xx/5xx
      showToast('API error: ' + message, 'error');
      break;
  }
});

📡 Live Event Log

Interact with any widget on this page — all events are captured and logged below in real time.

Event Log
Waiting for events…
javascript — log all widget events
['otp-setup-complete', 'otp-verified', 'otp-disabled', 'otp-error'].forEach(name => {
  document.addEventListener(name, (e) => {
    console.log(name, e.detail);
  });
});

🏗️ Combined Example — Security Settings Page

A realistic example showing how all three buttons work together on a user security settings page, with event handling.

⚙️ Security Settings alice@example.com
Two-Factor Authentication
Protect your account with an authenticator app
Verify Identity
Test your 2FA code to confirm it's working
Remove 2FA
Disable two-factor authentication (requires current code)
html — complete security settings panel
<!DOCTYPE html>
<html>
<head>
  <script src="https://otp.royaltycoding.com/otp-widget.js" defer></script>
</head>
<body>

<div class="settings-panel">

  <!-- Enable 2FA -->
  <div class="setting-row">
    <div class="setting-info">
      <h4>Two-Factor Authentication</h4>
      <p>Protect your account with an authenticator app</p>
    </div>
    <otp-setup-btn
      email="{{ user.email }}"
      api="https://otp.royaltycoding.com"
      icon="🔑"
      label="Enable 2FA">
    </otp-setup-btn>
  </div>

  <!-- Verify identity -->
  <div class="setting-row">
    <otp-verify-btn
      email="{{ user.email }}"
      api="https://otp.royaltycoding.com">
    </otp-verify-btn>
  </div>

  <!-- Remove 2FA (danger zone) -->
  <div class="setting-row danger">
    <otp-disable-btn
      email="{{ user.email }}"
      api="https://otp.royaltycoding.com"
      icon="🗑️"
      label="Remove 2FA">
    </otp-disable-btn>
  </div>

</div>

<script>
  document.addEventListener('otp-setup-complete', (e) => {
    alert('2FA enabled for: ' + e.detail.email);
    location.reload(); // Refresh to show 2FA status
  });

  document.addEventListener('otp-verified', (e) => {
    window.location.href = '/dashboard';
  });

  document.addEventListener('otp-disabled', (e) => {
    alert('2FA removed from your account.');
    location.reload();
  });

  document.addEventListener('otp-error', (e) => {
    console.error('[otp-error]', e.detail);
  });
</script>

</body>
</html>

🔴 Error Type Reference

errorTypeHTTP StatusCauseUX Behaviour
rate 429 Too many requests — DDoS protection triggered Fatal banner replaces modal body. User must close and wait.
cors 403 Origin not in allowed_origins table Fatal banner. Domain must be whitelisted by admin.
apierr 4xx / 5xx Wrong code, user not found, server error Digit inputs shake and reset. User can retry immediately.
network 0 (no response) Fetch failed — offline, DNS, timeout Fatal banner with network troubleshooting hint.

⚙️ Framework Integration Examples

React
jsx — React
// In your <head> or main entry:
// <script src="https://otp.royaltycoding.com/otp-widget.js"></script>

import { useEffect } from 'react';

export default function SecuritySettings({ user }) {
  useEffect(() => {
    const onSetup  = (e) => console.log('2FA on:', e.detail.email);
    const onVerify = (e) => window.location.href = '/dashboard';
    const onError  = (e) => console.error('OTP error:', e.detail);

    document.addEventListener('otp-setup-complete', onSetup);
    document.addEventListener('otp-verified', onVerify);
    document.addEventListener('otp-error', onError);

    return () => {
      document.removeEventListener('otp-setup-complete', onSetup);
      document.removeEventListener('otp-verified', onVerify);
      document.removeEventListener('otp-error', onError);
    };
  }, []);

  return (
    <div className="security-settings">
      <otp-setup-btn
        email={user.email}
        api="https://otp.royaltycoding.com"
        theme="dark"
      />
      <otp-verify-btn
        email={user.email}
        api="https://otp.royaltycoding.com"
      />
      <otp-disable-btn
        email={user.email}
        api="https://otp.royaltycoding.com"
        icon="⛔"
        label="Remove 2FA"
      />
    </div>
  );
}
Vue 3
vue — Vue 3
<template>
  <div>
    <otp-setup-btn
      :email="user.email"
      api="https://otp.royaltycoding.com"
      theme="dark"
    />
    <otp-verify-btn
      :email="user.email"
      api="https://otp.royaltycoding.com"
    />
    <otp-disable-btn
      :email="user.email"
      api="https://otp.royaltycoding.com"
    />
  </div>
</template>

<script setup>
import { onMounted, onUnmounted } from 'vue';

const props = defineProps(['user']);

const onSetup  = (e) => console.log('2FA enabled:', e.detail);
const onVerify = (e) => console.log('Verified:', e.detail);
const onError  = (e) => console.error('OTP error:', e.detail);

onMounted(() => {
  document.addEventListener('otp-setup-complete', onSetup);
  document.addEventListener('otp-verified', onVerify);
  document.addEventListener('otp-error', onError);
});

onUnmounted(() => {
  document.removeEventListener('otp-setup-complete', onSetup);
  document.removeEventListener('otp-verified', onVerify);
  document.removeEventListener('otp-error', onError);
});
</script>
PHP / Blade (Laravel)
blade — Laravel
<!-- In your Blade layout head -->
<script src="https://otp.royaltycoding.com/otp-widget.js" defer></script>

<!-- In your security settings view -->
<otp-setup-btn
  email="{{ auth()->user()->email }}"
  api="https://otp.royaltycoding.com"
  theme="dark">
</otp-setup-btn>

<otp-verify-btn
  email="{{ auth()->user()->email }}"
  api="https://otp.royaltycoding.com">
</otp-verify-btn>

<otp-disable-btn
  email="{{ auth()->user()->email }}"
  api="https://otp.royaltycoding.com"
  icon="⛔"
  label="Remove 2FA">
</otp-disable-btn>

<script>
  document.addEventListener('otp-setup-complete', (e) => {
    console.log('2FA enabled for:', e.detail.email);
  });
  document.addEventListener('otp-verified', (e) => {
    window.location.href = '/dashboard'; // proceed with login
  });
</script>

🔒 Securing Your API Token

Important
⚠️
Never put your token directly in HTML
Placing api-token="your-token" as a static HTML attribute makes it visible to anyone who views the page source or opens DevTools → Elements. Anyone can copy it and make API calls directly to your account.
✅ The right approach — inject from your server

Your token lives only in your server's environment variables. Your backend exposes a session-gated endpoint that returns the token only to authenticated users. Your frontend JS fetches it and sets it on the widget — the static HTML never contains the token.

Request flow
1. User logs in → your server sets a session cookie
2. Page loads → <otp-setup-btn api="..."></otp-setup-btn> ← no api-token here
3. JS runs → fetch('/otp-token') ← gated by your session middleware
4. Your server reads process.env.OTP_API_TOKEN → returns it
5. JS calls el.setAttribute('api-token', token) → widget works
Token never in HTML source, never in browser cache, never in CDN
Step 1 — Store the token on your server only
.env — your application server
# Create this token in your OTP API dashboard → Origins → API Tokens
# Never commit this file. Add .env to your .gitignore.
OTP_API_TOKEN=your-token-from-dashboard
Step 2 — Add a session-gated endpoint to your backend
This endpoint only returns the token to users who are already authenticated in your app. An unauthenticated visitor gets a 401 — they never see the token value.
javascript — Node.js / Express
// routes/otp.js
// requireAuth = your existing session/JWT middleware
app.get('/otp-token', requireAuth, (req, res) => {
  res.json({ token: process.env.OTP_API_TOKEN });
});
php — Laravel
// routes/web.php
Route::get('/otp-token', function () {
    return response()->json([
        'token' => config('services.otp_api.token')
    ]);
})->middleware('auth');
typescript — Next.js (App Router)
// app/api/otp-token/route.ts
import { getServerSession } from 'next-auth';
import { NextResponse } from 'next/server';

export async function GET() {
  const session = await getServerSession();
  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }
  return NextResponse.json({ token: process.env.OTP_API_TOKEN });
}
python — Django
# views.py
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
import os

@login_required
def otp_token(request):
    return JsonResponse({'token': os.environ.get('OTP_API_TOKEN')})
Step 3 — Fetch the token in JS and set it on the widget
The widget tags have no api-token attribute in the HTML. The attribute is only set after the authenticated fetch succeeds.
html + javascript
<!-- otp-widget.js loaded in <head> -->
<script src="https://otp.royaltycoding.com/otp-widget.js"></script>

<!-- No api-token here — token is injected by JS below -->
<otp-setup-btn
  id="otp-widget"
  api="https://otp.royaltycoding.com"
  email="user@example.com">
</otp-setup-btn>

<script>
(async function() {
  try {
    // Fetch token from your server — only works if user is logged in
    const res = await fetch('/otp-token');
    if (!res.ok) return; // user not authenticated — widget stays inactive

    const { token } = await res.json();

    // Set on all OTP widgets on the page
    document.querySelectorAll(
      'otp-setup-btn, otp-verify-btn, otp-disable-btn'
    ).forEach(el => el.setAttribute('api-token', token));

  } catch (e) {
    console.error('Could not load OTP token:', e);
  }
})();
</script>
jsx — React hook
import { useEffect, useRef } from 'react';

function OtpSetupButton({ email }) {
  const ref = useRef(null);

  useEffect(() => {
    async function injectToken() {
      const res = await fetch('/otp-token');
      if (!res.ok) return;
      const { token } = await res.json();
      if (ref.current) ref.current.setAttribute('api-token', token);
    }
    injectToken();
  }, []);

  return (
    <otp-setup-btn
      ref={ref}
      api="https://otp.royaltycoding.com"
      email={email}
    />
  );
}
What each layer protects against
Threat Protected? How
Token visible in HTML source ✅ Blocked Token injected via JS after page load — never in static HTML
Unauthenticated user fetches token ✅ Blocked Your /otp-token endpoint requires an active session
Token used from another website ✅ Blocked Token is origin-scoped — rejected if Origin header doesn't match
Token leaked via git / source control ✅ Blocked Token lives only in .env which is in .gitignore
Token visible in CDN / proxy cache ✅ Blocked Token is in a dynamic authenticated response, not static HTML
Token visible in DevTools Network tab ⚠️ Unavoidable Any authenticated user on their own device can see it — this is the browser's limit. Mitigated by origin-scoping + rate limits
Brute-force / abuse via stolen token ✅ Limited Rate-limited to 10 verify attempts per 5 min per IP via DDoS config
Summary: Server-side injection + session-gating stops the realistic attacks — source leaks, git exposure, CDN caching, and cross-origin abuse. The one thing you cannot fully hide from is a logged-in user on their own device inspecting their own network traffic, which is acceptable because that user already has legitimate access.