API Docs Security Widget Guide otp.royaltycoding.com
Widget Guide · v1.4

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

New in 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

New in 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-verified', (e) => {
    window.location.href = '{{ route("dashboard") }}';
  });
</script>
Vanilla JS — dynamic email from session
javascript — dynamic attribute
// Fetch user info and set email dynamically
const user = await fetch('/api/me').then(r => r.json());

document.querySelectorAll('otp-setup-btn, otp-verify-btn, otp-disable-btn')
  .forEach(el => el.setAttribute('email', user.email));

// You can also update icon or label at runtime:
const setupBtn = document.querySelector('otp-setup-btn');
setupBtn.setAttribute('icon', user.hasTOTP ? '✅' : '🔐');
setupBtn.setAttribute('label', user.hasTOTP ? '2FA Active' : 'Enable 2FA');