SaaS — API Keys & Webhooks
A developer-grade API keys and webhooks manager built in vanilla HTML, CSS, and JavaScript. The keys table shows masked secrets with reveal and copy, scopes, created date, and last used, while creation generates a fresh secret revealed only once and revoke asks for confirmation. A webhooks section lists endpoint URLs, subscribed events, and delivery status, lets you add new endpoints, and fires a test send that returns a simulated 200 OK, all with a working light and dark theme.
MCP
Code
:root {
--bg: #f7f8fb;
--surface: #ffffff;
--surface-2: #f1f3f9;
--ink: #0f1222;
--muted: #646b85;
--brand: #6366f1;
--brand-d: #4f46e5;
--ok: #16a34a;
--ok-bg: #e7f6ec;
--warn: #d97706;
--warn-bg: #fdf0dd;
--danger: #dc2626;
--danger-bg: #fdecec;
--line: rgba(15, 18, 34, .1);
--line-strong: rgba(15, 18, 34, .16);
--shadow-sm: 0 1px 2px rgba(15, 18, 34, .06);
--shadow-md: 0 8px 28px rgba(15, 18, 34, .12);
--shadow-lg: 0 20px 60px rgba(15, 18, 34, .25);
--radius: 12px;
--radius-sm: 8px;
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}
[data-theme="dark"] {
--bg: #0c0e16;
--surface: #14161f;
--surface-2: #1b1e2a;
--ink: #eef0f6;
--muted: #9aa0b8;
--line: rgba(255, 255, 255, .1);
--line-strong: rgba(255, 255, 255, .18);
--ok-bg: rgba(22, 163, 74, .16);
--warn-bg: rgba(217, 119, 6, .16);
--danger-bg: rgba(220, 38, 38, .16);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);
--shadow-md: 0 8px 28px rgba(0, 0, 0, .5);
--shadow-lg: 0 20px 60px rgba(0, 0, 0, .6);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.sr-only {
position: absolute; width: 1px; height: 1px;
padding: 0; margin: -1px; overflow: hidden;
clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.skip-link {
position: absolute; left: 12px; top: -48px;
background: var(--brand); color: #fff; padding: 8px 14px;
border-radius: var(--radius-sm); z-index: 100; transition: top .15s;
}
.skip-link:focus { top: 12px; }
:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
border-radius: 4px;
}
/* ---------- Topbar ---------- */
.topbar {
display: flex; align-items: center; justify-content: space-between;
gap: 16px; padding: 12px 20px;
background: var(--surface);
border-bottom: 1px solid var(--line);
position: sticky; top: 0; z-index: 20;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark {
display: grid; place-items: center;
width: 34px; height: 34px; border-radius: 10px;
background: linear-gradient(135deg, var(--brand), var(--brand-d));
color: #fff; box-shadow: var(--shadow-sm);
}
.brand-name { font-weight: 700; font-size: 16px; letter-spacing: -.2px; }
.brand-accent { color: var(--brand); }
.env-pill {
margin-left: 6px; font-size: 11px; font-weight: 600;
padding: 3px 9px; border-radius: 999px;
background: var(--ok-bg); color: var(--ok);
border: 1px solid color-mix(in srgb, var(--ok) 30%, transparent);
}
.topbar-actions { display: flex; align-items: center; gap: 14px; }
.theme-toggle {
width: 38px; height: 38px; border-radius: 10px;
border: 1px solid var(--line); background: var(--surface-2);
color: var(--ink); font-size: 17px; cursor: pointer;
display: grid; place-items: center; transition: background .15s, border-color .15s;
}
.theme-toggle:hover { border-color: var(--line-strong); }
.user-chip { display: flex; align-items: center; gap: 10px; }
.avatar {
width: 36px; height: 36px; border-radius: 50%;
display: grid; place-items: center; font-weight: 600; font-size: 13px;
background: linear-gradient(135deg, #f0abfc, #818cf8); color: #1a1037;
}
.user-meta { display: flex; flex-direction: column; line-height: 1.2; }
.user-name { font-weight: 600; font-size: 13px; }
.user-org { font-size: 11px; color: var(--muted); }
/* ---------- Layout ---------- */
.wrap { max-width: 1080px; margin: 0 auto; padding: 28px 20px 80px; }
.page-head { margin-bottom: 22px; }
.page-head h1 { margin: 0; font-size: 26px; letter-spacing: -.4px; }
.lede { margin: 6px 0 0; color: var(--muted); }
.muted { color: var(--muted); }
/* ---------- Tabs ---------- */
.tabs {
display: flex; gap: 4px; margin-bottom: 20px;
border-bottom: 1px solid var(--line);
}
.tab {
appearance: none; border: 0; background: none; cursor: pointer;
padding: 11px 16px; font: inherit; font-weight: 600; font-size: 14px;
color: var(--muted); border-bottom: 2px solid transparent;
margin-bottom: -1px; display: inline-flex; align-items: center; gap: 8px;
transition: color .15s;
}
.tab:hover { color: var(--ink); }
.tab.is-active { color: var(--brand); border-bottom-color: var(--brand); }
.tab-count {
font-size: 11px; font-weight: 700; min-width: 20px; padding: 1px 6px;
border-radius: 999px; background: var(--surface-2); color: var(--muted);
}
.tab.is-active .tab-count { background: color-mix(in srgb, var(--brand) 16%, transparent); color: var(--brand); }
/* ---------- Panels ---------- */
.panel { display: none; }
.panel.is-active { display: block; animation: fade .2s ease; }
@keyframes fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.panel-head {
display: flex; align-items: flex-end; justify-content: space-between;
gap: 16px; margin-bottom: 16px; flex-wrap: wrap;
}
.panel-head h2 { margin: 0; font-size: 18px; letter-spacing: -.2px; }
.panel-head .muted { margin: 4px 0 0; font-size: 13px; max-width: 52ch; }
/* ---------- Buttons ---------- */
.btn {
appearance: none; cursor: pointer; font: inherit; font-weight: 600; font-size: 13.5px;
border-radius: var(--radius-sm); padding: 9px 15px; border: 1px solid transparent;
display: inline-flex; align-items: center; gap: 7px; transition: background .15s, border-color .15s, transform .05s;
white-space: nowrap;
}
.btn:active { transform: translateY(1px); }
.btn-primary { background: var(--brand); color: #fff; box-shadow: var(--shadow-sm); }
.btn-primary:hover { background: var(--brand-d); }
.btn-ghost { background: transparent; color: var(--ink); border-color: var(--line); }
.btn-ghost:hover { background: var(--surface-2); }
.btn-soft { background: var(--surface-2); color: var(--ink); border-color: var(--line); }
.btn-soft:hover { border-color: var(--line-strong); }
.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover { background: #b91c1c; }
.icon-btn {
appearance: none; cursor: pointer; border: 0; background: none;
width: 32px; height: 32px; border-radius: 8px; color: var(--muted);
font-size: 15px; display: grid; place-items: center; transition: background .15s, color .15s;
}
.icon-btn:hover { background: var(--surface-2); color: var(--ink); }
/* ---------- Keys table ---------- */
.table-card {
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius); box-shadow: var(--shadow-sm); overflow: hidden;
}
.data-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.data-table thead th {
text-align: left; font-size: 11px; font-weight: 600; text-transform: uppercase;
letter-spacing: .5px; color: var(--muted); padding: 11px 16px;
background: var(--surface-2); border-bottom: 1px solid var(--line);
}
.data-table tbody td { padding: 13px 16px; border-bottom: 1px solid var(--line); vertical-align: middle; }
.data-table tbody tr:last-child td { border-bottom: 0; }
.data-table tbody tr { transition: background .12s; }
.data-table tbody tr:hover { background: var(--surface-2); }
.key-name { font-weight: 600; }
.key-mask { display: inline-flex; align-items: center; gap: 6px; }
.key-mono {
font-family: var(--mono); font-size: 12.5px;
background: var(--surface-2); padding: 4px 8px; border-radius: 6px;
border: 1px solid var(--line); color: var(--ink); letter-spacing: .3px;
}
.row-actions { display: inline-flex; gap: 4px; }
.scope-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.scope-chip {
font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 999px;
background: color-mix(in srgb, var(--brand) 12%, transparent); color: var(--brand-d);
border: 1px solid color-mix(in srgb, var(--brand) 24%, transparent);
}
[data-theme="dark"] .scope-chip { color: #a5b4fc; }
.scope-chip[data-s="admin"] { background: var(--danger-bg); color: var(--danger); border-color: color-mix(in srgb, var(--danger) 30%, transparent); }
.scope-chip[data-s="billing"] { background: var(--warn-bg); color: var(--warn); border-color: color-mix(in srgb, var(--warn) 30%, transparent); }
.cell-dim { color: var(--muted); white-space: nowrap; }
.cell-dim .never { font-style: italic; opacity: .8; }
/* ---------- Webhooks ---------- */
.hooks-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; }
.hook-card {
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius); box-shadow: var(--shadow-sm);
padding: 16px; display: flex; flex-direction: column; gap: 12px;
}
.hook-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.hook-url {
font-family: var(--mono); font-size: 12.5px; font-weight: 500;
word-break: break-all; color: var(--ink);
}
.status-dot {
display: inline-flex; align-items: center; gap: 6px;
font-size: 11px; font-weight: 600; padding: 3px 9px; border-radius: 999px;
white-space: nowrap;
}
.status-dot::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.status-active { background: var(--ok-bg); color: var(--ok); }
.status-paused { background: var(--surface-2); color: var(--muted); }
.hook-events { display: flex; flex-wrap: wrap; gap: 5px; }
.event-chip {
font-family: var(--mono); font-size: 11px; padding: 2px 7px; border-radius: 6px;
background: var(--surface-2); border: 1px solid var(--line); color: var(--muted);
}
.hook-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 2px; }
.hook-meta { font-size: 12px; color: var(--muted); }
.hook-meta .resp-200 { color: var(--ok); font-weight: 600; }
.hook-actions { display: flex; gap: 6px; }
.btn-xs { padding: 6px 11px; font-size: 12.5px; }
/* ---------- Empty ---------- */
.empty { text-align: center; padding: 48px 20px; color: var(--muted); }
.empty-art { font-size: 34px; margin-bottom: 10px; }
.empty p { margin: 0; }
/* ---------- Modals ---------- */
.modal-scrim {
position: fixed; inset: 0; background: rgba(8, 9, 16, .5);
backdrop-filter: blur(2px); display: grid; place-items: center;
z-index: 50; padding: 20px; animation: fade .15s ease;
}
.modal {
width: 100%; max-width: 460px; background: var(--surface);
border: 1px solid var(--line); border-radius: 16px;
box-shadow: var(--shadow-lg); overflow: hidden; animation: pop .18s ease;
}
.modal-sm { max-width: 400px; }
@keyframes pop { from { opacity: 0; transform: scale(.96) translateY(6px); } to { opacity: 1; transform: none; } }
.modal-head {
display: flex; align-items: center; justify-content: space-between;
padding: 16px 20px; border-bottom: 1px solid var(--line);
}
.modal-head h3 { margin: 0; font-size: 16px; }
.modal-body { padding: 20px; display: flex; flex-direction: column; gap: 16px; }
.modal-foot {
display: flex; justify-content: flex-end; gap: 10px;
padding: 14px 20px; border-top: 1px solid var(--line); background: var(--surface-2);
}
.modal-body + .modal-foot { border-top: 1px solid var(--line); }
.field { display: flex; flex-direction: column; gap: 7px; border: 0; padding: 0; margin: 0; min-width: 0; }
.field-label { font-size: 13px; font-weight: 600; }
.field input[type="text"],
.field input[type="url"] {
font: inherit; font-size: 14px; padding: 10px 12px; width: 100%;
border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
background: var(--bg); color: var(--ink);
}
.field input::placeholder { color: var(--muted); }
.field input:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; border-color: var(--brand); }
.scopes { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.check {
display: flex; align-items: center; gap: 8px; font-size: 13.5px;
padding: 9px 11px; border: 1px solid var(--line); border-radius: var(--radius-sm);
cursor: pointer; background: var(--bg); transition: border-color .15s;
font-family: var(--mono); font-weight: 500;
}
.check:hover { border-color: var(--line-strong); }
.check input { accent-color: var(--brand); width: 15px; height: 15px; }
.check:has(input:checked) { border-color: var(--brand); background: color-mix(in srgb, var(--brand) 8%, var(--bg)); }
.warn-banner {
display: flex; gap: 10px; align-items: flex-start; font-size: 13px;
background: var(--warn-bg); color: var(--warn);
border: 1px solid color-mix(in srgb, var(--warn) 30%, transparent);
padding: 12px 14px; border-radius: var(--radius-sm); line-height: 1.45;
}
.warn-banner strong { color: inherit; }
.secret-box { display: flex; gap: 8px; align-items: stretch; }
.secret-code {
flex: 1; min-width: 0; font-family: var(--mono); font-size: 13px;
background: var(--bg); border: 1px solid var(--line-strong);
border-radius: var(--radius-sm); padding: 11px 12px; color: var(--ink);
word-break: break-all; display: flex; align-items: center;
}
/* ---------- Toasts ---------- */
.toast-host {
position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%);
display: flex; flex-direction: column; gap: 8px; z-index: 80; width: max-content; max-width: 90vw;
}
.toast {
background: var(--ink); color: var(--bg);
padding: 11px 16px; border-radius: 10px; font-size: 13.5px; font-weight: 500;
box-shadow: var(--shadow-md); display: flex; align-items: center; gap: 9px;
animation: toastIn .22s ease;
}
.toast.is-out { animation: toastOut .25s ease forwards; }
.toast .tdot { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); flex: none; }
.toast.toast-err .tdot { background: #f87171; }
@keyframes toastIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes toastOut { to { opacity: 0; transform: translateY(8px); } }
/* ---------- Responsive ---------- */
@media (max-width: 720px) {
.user-meta { display: none; }
.panel-head { align-items: stretch; }
.panel-head .btn { align-self: flex-start; }
.data-table thead { display: none; }
.data-table, .data-table tbody, .data-table tr, .data-table td { display: block; width: 100%; }
.data-table tbody tr { padding: 12px 14px; border-bottom: 1px solid var(--line); }
.data-table tbody tr:hover { background: transparent; }
.data-table tbody td { padding: 5px 0; border: 0; display: flex; justify-content: space-between; gap: 12px; align-items: center; }
.data-table tbody td::before {
content: attr(data-label); font-size: 11px; font-weight: 600;
text-transform: uppercase; letter-spacing: .4px; color: var(--muted);
}
.data-table td .scope-chips, .data-table td .key-mask, .data-table td .row-actions { justify-content: flex-end; flex-wrap: wrap; }
}
@media (max-width: 420px) {
.wrap { padding: 20px 14px 64px; }
.page-head h1 { font-size: 22px; }
.scopes { grid-template-columns: 1fr; }
.hooks-grid { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}(function () {
"use strict";
/* ---------- State ---------- */
var keys = [
{ id: "k1", name: "Production server", prefix: "vk_live", tail: "8fA2", scopes: ["read", "write", "billing"], created: "Mar 12, 2026", lastUsed: "2 hours ago" },
{ id: "k2", name: "Analytics reader", prefix: "vk_live", tail: "q4Lp", scopes: ["read"], created: "Feb 28, 2026", lastUsed: "Yesterday" },
{ id: "k3", name: "Staging CI", prefix: "vk_test", tail: "Zr19", scopes: ["read", "write"], created: "Jan 09, 2026", lastUsed: null }
];
var hooks = [
{ id: "h1", url: "https://api.helixlabs.dev/hooks/stripe", events: ["invoice.paid", "subscription.updated"], status: "active", lastResp: 200, lastAt: "5 min ago" },
{ id: "h2", url: "https://hooks.helixlabs.dev/audit-log", events: ["customer.created"], status: "paused", lastResp: null, lastAt: null }
];
var pendingRevoke = null;
/* ---------- Helpers ---------- */
var $ = function (s, r) { return (r || document).querySelector(s); };
function el(tag, cls, html) {
var n = document.createElement(tag);
if (cls) n.className = cls;
if (html != null) n.innerHTML = html;
return n;
}
function esc(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
});
}
var toastHost = $("#toastHost");
function toast(msg, type) {
var t = el("div", "toast" + (type === "err" ? " toast-err" : ""));
t.appendChild(el("span", "tdot"));
t.appendChild(document.createTextNode(msg));
toastHost.appendChild(t);
setTimeout(function () {
t.classList.add("is-out");
t.addEventListener("animationend", function () { t.remove(); });
}, 2600);
}
function genSecret(prefix) {
var alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var body = "";
for (var i = 0; i < 32; i++) body += alpha[Math.floor(Math.random() * alpha.length)];
return (prefix || "vk_live") + "_" + body;
}
function copyText(text, okMsg) {
var done = function () { toast(okMsg || "Copied to clipboard"); };
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done).catch(fallback);
} else { fallback(); }
function fallback() {
var ta = el("textarea");
ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0";
document.body.appendChild(ta); ta.select();
try { document.execCommand("copy"); done(); } catch (e) { toast("Copy failed", "err"); }
ta.remove();
}
}
/* ---------- Render: keys ---------- */
var keysBody = $("#keysBody");
var keysEmpty = $("#keysEmpty");
function renderKeys() {
keysBody.innerHTML = "";
$("#keyCount").textContent = keys.length;
keysEmpty.hidden = keys.length > 0;
keys.forEach(function (k) {
var tr = el("tr");
tr.dataset.id = k.id;
var tdName = el("td", "", '<span class="key-name">' + esc(k.name) + "</span>");
tdName.setAttribute("data-label", "Name");
var masked = k.prefix + "_" + "••••••••" + k.tail;
var tdKey = el("td");
tdKey.setAttribute("data-label", "Key");
var wrap = el("span", "key-mask");
var code = el("code", "key-mono");
code.textContent = masked;
code.dataset.revealed = "0";
var revealBtn = el("button", "icon-btn", "👁");
revealBtn.title = "Reveal key";
revealBtn.setAttribute("aria-label", "Reveal key");
revealBtn.addEventListener("click", function () {
if (code.dataset.revealed === "0") {
code.textContent = genStableReveal(k);
code.dataset.revealed = "1";
revealBtn.textContent = "🙈";
revealBtn.title = "Hide key";
} else {
code.textContent = masked;
code.dataset.revealed = "0";
revealBtn.textContent = "👁";
revealBtn.title = "Reveal key";
}
});
var copyBtn = el("button", "icon-btn", "⧉");
copyBtn.title = "Copy key";
copyBtn.setAttribute("aria-label", "Copy key");
copyBtn.addEventListener("click", function () { copyText(genStableReveal(k), "Key copied"); });
wrap.appendChild(code); wrap.appendChild(revealBtn); wrap.appendChild(copyBtn);
tdKey.appendChild(wrap);
var tdScopes = el("td");
tdScopes.setAttribute("data-label", "Scopes");
var chips = el("div", "scope-chips");
k.scopes.forEach(function (s) {
var c = el("span", "scope-chip", esc(s));
c.dataset.s = s;
chips.appendChild(c);
});
tdScopes.appendChild(chips);
var tdCreated = el("td", "cell-dim", esc(k.created));
tdCreated.setAttribute("data-label", "Created");
var tdUsed = el("td", "cell-dim", k.lastUsed ? esc(k.lastUsed) : '<span class="never">Never</span>');
tdUsed.setAttribute("data-label", "Last used");
var tdAct = el("td");
tdAct.setAttribute("data-label", "");
var acts = el("div", "row-actions");
var revoke = el("button", "icon-btn", "🗑");
revoke.title = "Revoke key";
revoke.setAttribute("aria-label", "Revoke key " + k.name);
revoke.addEventListener("click", function () { askRevoke(k.id); });
acts.appendChild(revoke);
tdAct.appendChild(acts);
tr.append(tdName, tdKey, tdScopes, tdCreated, tdUsed, tdAct);
keysBody.appendChild(tr);
});
}
// Deterministic full value per key for reveal/copy demo
var revealCache = {};
function genStableReveal(k) {
if (!revealCache[k.id]) {
var pad = "";
var seed = (k.id + k.name).split("").reduce(function (a, c) { return a + c.charCodeAt(0); }, 0);
var alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < 28; i++) { seed = (seed * 33 + i) % alpha.length; pad += alpha[seed]; }
revealCache[k.id] = k.prefix + "_" + pad + k.tail;
}
return revealCache[k.id];
}
/* ---------- Render: webhooks ---------- */
var hooksGrid = $("#hooksGrid");
var hooksEmpty = $("#hooksEmpty");
function renderHooks() {
hooksGrid.innerHTML = "";
$("#hookCount").textContent = hooks.length;
hooksEmpty.hidden = hooks.length > 0;
hooks.forEach(function (h) {
var card = el("div", "hook-card");
card.dataset.id = h.id;
var top = el("div", "hook-top");
top.appendChild(el("div", "hook-url", esc(h.url)));
var statusCls = h.status === "active" ? "status-active" : "status-paused";
top.appendChild(el("span", "status-dot " + statusCls, h.status === "active" ? "Active" : "Paused"));
card.appendChild(top);
var ev = el("div", "hook-events");
h.events.forEach(function (e) { ev.appendChild(el("span", "event-chip", esc(e))); });
card.appendChild(ev);
var foot = el("div", "hook-foot");
var meta = el("div", "hook-meta");
if (h.lastResp) {
meta.innerHTML = "Last delivery <span class=\"resp-200\">" + h.lastResp + " OK</span> · " + esc(h.lastAt);
} else {
meta.textContent = "No deliveries yet";
}
foot.appendChild(meta);
var actions = el("div", "hook-actions");
var testBtn = el("button", "btn btn-soft btn-xs", "Send test");
testBtn.addEventListener("click", function () { testSend(h, testBtn, meta); });
var delBtn = el("button", "btn btn-ghost btn-xs", "Delete");
delBtn.addEventListener("click", function () {
hooks = hooks.filter(function (x) { return x.id !== h.id; });
renderHooks();
toast("Endpoint deleted");
});
actions.append(testBtn, delBtn);
foot.appendChild(actions);
card.appendChild(foot);
hooksGrid.appendChild(card);
});
}
function testSend(h, btn, meta) {
btn.disabled = true;
var orig = btn.textContent;
btn.textContent = "Sending…";
setTimeout(function () {
btn.disabled = false;
btn.textContent = orig;
h.lastResp = 200;
h.lastAt = "just now";
meta.innerHTML = "Last delivery <span class=\"resp-200\">200 OK</span> · just now";
toast("Test event delivered — 200 OK");
}, 850);
}
/* ---------- Tabs ---------- */
var tabs = document.querySelectorAll(".tab");
tabs.forEach(function (tab) {
tab.addEventListener("click", function () {
tabs.forEach(function (t) { t.classList.remove("is-active"); t.setAttribute("aria-selected", "false"); });
tab.classList.add("is-active");
tab.setAttribute("aria-selected", "true");
var name = tab.dataset.tab;
document.querySelectorAll(".panel").forEach(function (p) {
var match = p.id === "panel-" + name;
p.classList.toggle("is-active", match);
p.hidden = !match;
});
});
});
/* ---------- Modal plumbing ---------- */
var lastFocus = null;
function openModal(id) {
lastFocus = document.activeElement;
var m = document.getElementById(id);
m.hidden = false;
var focusable = m.querySelector("input, button, [tabindex]");
if (focusable) focusable.focus();
}
function closeModal(id) {
document.getElementById(id).hidden = true;
if (lastFocus && lastFocus.focus) lastFocus.focus();
}
document.querySelectorAll("[data-close]").forEach(function (b) {
b.addEventListener("click", function () { closeModal(b.dataset.close); });
});
document.querySelectorAll(".modal-scrim").forEach(function (s) {
s.addEventListener("click", function (e) { if (e.target === s) s.hidden = true; });
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") {
document.querySelectorAll(".modal-scrim:not([hidden])").forEach(function (s) { s.hidden = true; });
}
});
/* ---------- Create key ---------- */
$("#newKeyBtn").addEventListener("click", function () {
$("#keyForm").reset();
openModal("keyModal");
});
$("#keyForm").addEventListener("submit", function (e) {
e.preventDefault();
var name = $("#keyName").value.trim();
if (!name) { toast("Give your key a name", "err"); return; }
var scopes = Array.prototype.map.call(
document.querySelectorAll('#keyForm input[name="scope"]:checked'),
function (c) { return c.value; }
);
if (!scopes.length) scopes = ["read"];
var prefix = "vk_live";
var secret = genSecret(prefix);
var tail = secret.slice(-4);
var newKey = {
id: "k" + Date.now(),
name: name,
prefix: prefix,
tail: tail,
scopes: scopes,
created: new Date().toLocaleDateString("en-US", { month: "short", day: "2-digit", year: "numeric" }),
lastUsed: null
};
revealCache[newKey.id] = secret; // reveal/copy will show the real secret
keys.unshift(newKey);
renderKeys();
closeModal("keyModal");
// Show secret ONCE
$("#secretValue").textContent = secret;
openModal("secretModal");
});
$("#copySecret").addEventListener("click", function () {
copyText($("#secretValue").textContent, "Secret copied — store it safely");
});
$("#secretDone").addEventListener("click", function () {
closeModal("secretModal");
toast("Key created");
});
/* ---------- Revoke ---------- */
function askRevoke(id) {
pendingRevoke = id;
var k = keys.find(function (x) { return x.id === id; });
$("#confirmText").textContent = "Revoking “" + k.name + "” immediately disables it. Any request using it will fail with 401.";
openModal("confirmModal");
}
$("#confirmRevoke").addEventListener("click", function () {
if (pendingRevoke) {
keys = keys.filter(function (x) { return x.id !== pendingRevoke; });
renderKeys();
toast("Key revoked");
pendingRevoke = null;
}
closeModal("confirmModal");
});
/* ---------- Add webhook ---------- */
$("#newHookBtn").addEventListener("click", function () {
$("#hookForm").reset();
openModal("hookModal");
});
$("#hookForm").addEventListener("submit", function (e) {
e.preventDefault();
var url = $("#hookUrl").value.trim();
if (!/^https?:\/\/.+/.test(url)) { toast("Enter a valid URL", "err"); return; }
var events = Array.prototype.map.call(
document.querySelectorAll('#hookForm input[name="event"]:checked'),
function (c) { return c.value; }
);
if (!events.length) events = ["invoice.paid"];
hooks.unshift({
id: "h" + Date.now(),
url: url,
events: events,
status: "active",
lastResp: null,
lastAt: null
});
renderHooks();
closeModal("hookModal");
toast("Endpoint added");
});
/* ---------- Theme ---------- */
var themeBtn = $("#themeToggle");
themeBtn.addEventListener("click", function () {
var dark = document.documentElement.getAttribute("data-theme") === "dark";
document.documentElement.setAttribute("data-theme", dark ? "light" : "dark");
themeBtn.setAttribute("aria-pressed", String(!dark));
});
/* ---------- Init ---------- */
renderKeys();
renderHooks();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>API Keys & Webhooks — Stealthis</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="topbar" role="banner">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>
</svg>
</span>
<span class="brand-name">Vault<span class="brand-accent">API</span></span>
<span class="env-pill" title="Current environment">Production</span>
</div>
<div class="topbar-actions">
<button class="theme-toggle" id="themeToggle" type="button" aria-pressed="false" title="Toggle theme">
<span class="theme-icon" aria-hidden="true">◐</span>
<span class="sr-only">Toggle dark mode</span>
</button>
<div class="user-chip">
<span class="avatar" aria-hidden="true">AK</span>
<span class="user-meta">
<span class="user-name">Ada Keys</span>
<span class="user-org">Helix Labs</span>
</span>
</div>
</div>
</header>
<main id="main" class="wrap" role="main">
<div class="page-head">
<div>
<h1>API Keys & Webhooks</h1>
<p class="lede">Manage programmatic access and delivery of real-time events for your workspace.</p>
</div>
</div>
<nav class="tabs" aria-label="Sections">
<button class="tab is-active" type="button" role="tab" aria-selected="true" data-tab="keys">
API Keys <span class="tab-count" id="keyCount">3</span>
</button>
<button class="tab" type="button" role="tab" aria-selected="false" data-tab="hooks">
Webhooks <span class="tab-count" id="hookCount">2</span>
</button>
</nav>
<!-- KEYS PANEL -->
<section class="panel is-active" id="panel-keys" role="tabpanel" aria-label="API keys">
<div class="panel-head">
<div class="panel-head-text">
<h2>Secret keys</h2>
<p class="muted">Keys carry the permissions of the user who created them. Treat them like passwords.</p>
</div>
<button class="btn btn-primary" id="newKeyBtn" type="button">
<span aria-hidden="true">+</span> Create key
</button>
</div>
<div class="table-card">
<table class="data-table" id="keysTable">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Key</th>
<th scope="col">Scopes</th>
<th scope="col">Created</th>
<th scope="col">Last used</th>
<th scope="col"><span class="sr-only">Actions</span></th>
</tr>
</thead>
<tbody id="keysBody"><!-- rows injected --></tbody>
</table>
<div class="empty" id="keysEmpty" hidden>
<div class="empty-art" aria-hidden="true">🔑</div>
<p>No active keys. Create one to start calling the API.</p>
</div>
</div>
</section>
<!-- WEBHOOKS PANEL -->
<section class="panel" id="panel-hooks" role="tabpanel" aria-label="Webhooks" hidden>
<div class="panel-head">
<div class="panel-head-text">
<h2>Webhook endpoints</h2>
<p class="muted">We POST event payloads to these URLs and retry with backoff on failure.</p>
</div>
<button class="btn btn-primary" id="newHookBtn" type="button">
<span aria-hidden="true">+</span> Add endpoint
</button>
</div>
<div class="hooks-grid" id="hooksGrid"><!-- cards injected --></div>
<div class="empty" id="hooksEmpty" hidden>
<div class="empty-art" aria-hidden="true">📡</div>
<p>No endpoints yet. Add one to receive events.</p>
</div>
</section>
</main>
<!-- CREATE KEY DIALOG -->
<div class="modal-scrim" id="keyModal" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="keyModalTitle">
<header class="modal-head">
<h3 id="keyModalTitle">Create API key</h3>
<button class="icon-btn" type="button" data-close="keyModal" aria-label="Close">✕</button>
</header>
<form id="keyForm" class="modal-body">
<label class="field">
<span class="field-label">Key name</span>
<input type="text" id="keyName" name="keyName" placeholder="e.g. Billing service" maxlength="40" required autocomplete="off" />
</label>
<fieldset class="field">
<legend class="field-label">Scopes</legend>
<div class="scopes">
<label class="check"><input type="checkbox" name="scope" value="read" checked /> <span>read</span></label>
<label class="check"><input type="checkbox" name="scope" value="write" /> <span>write</span></label>
<label class="check"><input type="checkbox" name="scope" value="billing" /> <span>billing</span></label>
<label class="check"><input type="checkbox" name="scope" value="admin" /> <span>admin</span></label>
</div>
</fieldset>
<footer class="modal-foot">
<button class="btn btn-ghost" type="button" data-close="keyModal">Cancel</button>
<button class="btn btn-primary" type="submit">Generate key</button>
</footer>
</form>
</div>
</div>
<!-- SECRET SHOWN ONCE DIALOG -->
<div class="modal-scrim" id="secretModal" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="secretModalTitle">
<header class="modal-head">
<h3 id="secretModalTitle">Save your secret key</h3>
</header>
<div class="modal-body">
<div class="warn-banner" role="alert">
<span aria-hidden="true">⚠️</span>
This key is shown <strong>only once</strong>. Copy it now — you won't be able to see it again.
</div>
<div class="secret-box">
<code id="secretValue" class="secret-code"></code>
<button class="btn btn-soft" type="button" id="copySecret">Copy</button>
</div>
</div>
<footer class="modal-foot">
<button class="btn btn-primary" type="button" id="secretDone">I've saved it</button>
</footer>
</div>
</div>
<!-- ADD WEBHOOK DIALOG -->
<div class="modal-scrim" id="hookModal" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="hookModalTitle">
<header class="modal-head">
<h3 id="hookModalTitle">Add webhook endpoint</h3>
<button class="icon-btn" type="button" data-close="hookModal" aria-label="Close">✕</button>
</header>
<form id="hookForm" class="modal-body">
<label class="field">
<span class="field-label">Endpoint URL</span>
<input type="url" id="hookUrl" name="hookUrl" placeholder="https://api.example.com/webhooks" required autocomplete="off" />
</label>
<fieldset class="field">
<legend class="field-label">Events to send</legend>
<div class="scopes">
<label class="check"><input type="checkbox" name="event" value="invoice.paid" checked /> <span>invoice.paid</span></label>
<label class="check"><input type="checkbox" name="event" value="customer.created" /> <span>customer.created</span></label>
<label class="check"><input type="checkbox" name="event" value="subscription.updated" /> <span>subscription.updated</span></label>
<label class="check"><input type="checkbox" name="event" value="payout.failed" /> <span>payout.failed</span></label>
</div>
</fieldset>
<footer class="modal-foot">
<button class="btn btn-ghost" type="button" data-close="hookModal">Cancel</button>
<button class="btn btn-primary" type="submit">Add endpoint</button>
</footer>
</form>
</div>
</div>
<!-- CONFIRM DIALOG -->
<div class="modal-scrim" id="confirmModal" hidden>
<div class="modal modal-sm" role="alertdialog" aria-modal="true" aria-labelledby="confirmTitle" aria-describedby="confirmText">
<header class="modal-head">
<h3 id="confirmTitle">Revoke key?</h3>
</header>
<div class="modal-body">
<p id="confirmText" class="muted">This immediately disables the key. Any request using it will fail with 401.</p>
</div>
<footer class="modal-foot">
<button class="btn btn-ghost" type="button" data-close="confirmModal">Cancel</button>
<button class="btn btn-danger" type="button" id="confirmRevoke">Revoke</button>
</footer>
</div>
</div>
<div class="toast-host" id="toastHost" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>API Keys & Webhooks
A focused settings surface for the part of a SaaS product developers actually touch: programmatic credentials and event delivery. The keys table lists each secret with a masked value you can reveal or copy, its granted scopes shown as colour-coded chips, the creation date, and a relative last-used timestamp. Creating a key opens a dialog for a name and scopes, then surfaces the freshly generated secret in a one-time reveal panel with a prominent warning — copy it now or lose it. Revoking always routes through a confirmation step so a misclick never disables production traffic.
The webhooks section presents each endpoint as a card with its URL, subscribed events, live active or paused status, and the result of its last delivery. Adding an endpoint validates the URL and lets you pick which events to subscribe to. The Send test button simulates a delivery with a brief loading state and returns a 200 OK, updating the card and raising a toast — the kind of immediate feedback developers expect.
Everything works without a backend: keys and webhooks live in memory, dialogs trap focus and close on Escape or outside click, toasts confirm each action, and a theme toggle gives full light and dark parity. The layout collapses the table into stacked, labelled rows on small screens and reflows the webhook cards into a single column, with landmark roles, aria attributes, and visible focus rings throughout.
Illustrative SaaS UI only — fictional product, metrics, and billing. No real backend.