Upload files to "/"
This commit is contained in:
+723
@@ -0,0 +1,723 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<script>
|
||||
(function(){var t=localStorage.getItem('qrknit-theme');if(t&&t!=='default')document.documentElement.setAttribute('data-theme',t);})();
|
||||
</script>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API Reference — QRknit</title>
|
||||
<meta name="description" content="Complete REST API reference for QRknit. Endpoints for authentication, link management, QR code generation, analytics, and admin operations.">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="https://qrknit.com/api-docs">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://qrknit.com/api-docs">
|
||||
<meta property="og:site_name" content="QRknit">
|
||||
<meta property="og:title" content="API Reference — QRknit">
|
||||
<meta property="og:description" content="Complete REST API reference for QRknit. Endpoints for authentication, link management, QR code generation, analytics, and admin operations.">
|
||||
<meta property="og:image" content="https://qrknit.com/static/og-image.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="/static/qk-ico.png">
|
||||
<link rel="apple-touch-icon" href="/static/qk-ico.png">
|
||||
|
||||
<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=Orbitron:wght@400;700;900&family=Share+Tech+Mono&family=Barlow:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #0a0a0f;
|
||||
--surface: #111118;
|
||||
--surface2: #181824;
|
||||
--surface3: #1e1e2e;
|
||||
--border: rgba(255,255,255,0.07);
|
||||
--accent: #00b8d4;
|
||||
--accent-rgb: 0,184,212;
|
||||
--accent-grad: #1a52c8;
|
||||
--accent-grad-rgb: 26,82,200;
|
||||
--accent2: #ff5e62;
|
||||
--accent3: #7c6af7;
|
||||
--accent3-rgb: 124,106,247;
|
||||
--accent3-light: #a89cf7;
|
||||
--text: #f0f0f8;
|
||||
--muted: #808098;
|
||||
--radius: 12px;
|
||||
--font-head: 'Orbitron', sans-serif;
|
||||
--font-mono: 'Share Tech Mono', monospace;
|
||||
--font-body: 'Barlow', sans-serif;
|
||||
}
|
||||
[data-theme="gold"] { --accent:#f5a623; --accent-rgb:245,166,35; --accent-grad:#c47f17; --accent-grad-rgb:196,127,23; --accent3:#d4a847; --accent3-rgb:212,168,71; --accent3-light:#e8c96a; }
|
||||
[data-theme="green"] { --accent:#00c896; --accent-rgb:0,200,150; --accent-grad:#007a58; --accent-grad-rgb:0,122,88; --accent3:#4caf7a; --accent3-rgb:76,175,122; --accent3-light:#7fd9a4; }
|
||||
[data-theme="red"] { --accent:#ff4757; --accent-rgb:255,71,87; --accent-grad:#c0392b; --accent-grad-rgb:192,57,43; --accent3:#ff6b7a; --accent3-rgb:255,107,122; --accent3-light:#ff9daa; }
|
||||
|
||||
html { scroll-behavior: smooth; }
|
||||
body { background:var(--bg); color:var(--text); font-family:var(--font-body); font-size:15px; line-height:1.6; min-height:100vh; overflow-x:hidden; }
|
||||
body::before {
|
||||
content:''; position:fixed; inset:0; pointer-events:none; z-index:0;
|
||||
background:
|
||||
radial-gradient(ellipse 90% 60% at 15% -5%, rgba(var(--accent-rgb),.09) 0%, transparent 60%),
|
||||
radial-gradient(ellipse 60% 50% at 90% 90%, rgba(var(--accent-grad-rgb),.08) 0%, transparent 55%),
|
||||
radial-gradient(ellipse 50% 50% at 50% 50%, rgba(var(--accent-rgb),.03) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
/* ── HEADER ── */
|
||||
header {
|
||||
position:sticky; top:0; z-index:200;
|
||||
display:flex; align-items:center; justify-content:space-between;
|
||||
padding:0 40px; height:64px;
|
||||
background:rgba(10,10,15,.88); backdrop-filter:blur(20px);
|
||||
border-bottom:1px solid var(--border);
|
||||
}
|
||||
.logo { font-family:var(--font-head); font-size:18px; font-weight:700; letter-spacing:.04em; text-decoration:none; color:var(--text); display:inline-flex; align-items:center; gap:8px; }
|
||||
.logo-dot-text { color:var(--accent); text-shadow:0 0 12px var(--accent); }
|
||||
.logo-icon-wrap { position:relative; display:inline-flex; align-items:center; flex-shrink:0; isolation:isolate; }
|
||||
.logo-icon { height:26px; width:26px; object-fit:contain; border-radius:3px; display:block; }
|
||||
.logo-icon-tint { position:absolute; inset:0; background:var(--accent); mix-blend-mode:hue; border-radius:3px; pointer-events:none; }
|
||||
|
||||
.header-right { display:flex; align-items:center; gap:12px; }
|
||||
.theme-picker { display:flex; align-items:center; gap:6px; padding:0 4px; }
|
||||
.theme-swatch { width:14px; height:14px; border-radius:50%; border:2px solid transparent; cursor:pointer; padding:0; transition:transform .2s,border-color .2s; }
|
||||
.theme-swatch:hover { transform:scale(1.2); }
|
||||
.theme-swatch.active { border-color:var(--text); transform:scale(1.15); }
|
||||
.theme-swatch[data-theme="default"] { background:linear-gradient(135deg,#00b8d4,#1a52c8); }
|
||||
.theme-swatch[data-theme="gold"] { background:linear-gradient(135deg,#f5a623,#c47f17); }
|
||||
.theme-swatch[data-theme="green"] { background:linear-gradient(135deg,#00c896,#007a58); }
|
||||
.theme-swatch[data-theme="red"] { background:linear-gradient(135deg,#ff4757,#c0392b); }
|
||||
|
||||
/* ── BUTTONS ── */
|
||||
.btn-primary { background:linear-gradient(135deg,var(--accent),var(--accent-grad)); color:#fff; border:none; cursor:pointer; font-family:var(--font-head); font-weight:700; font-size:12px; letter-spacing:.04em; padding:12px 22px; border-radius:var(--radius); transition:all .2s; white-space:nowrap; text-decoration:none; display:inline-flex; align-items:center; gap:6px; }
|
||||
.btn-primary:hover { transform:translateY(-1px); box-shadow:0 8px 24px rgba(var(--accent-rgb),.35); }
|
||||
.btn-outline { background:transparent; border:1px solid var(--border); color:var(--text); font-family:var(--font-mono); font-size:12px; padding:11px 20px; border-radius:var(--radius); cursor:pointer; transition:all .2s; text-decoration:none; display:inline-flex; align-items:center; gap:6px; white-space:nowrap; }
|
||||
.btn-outline:hover { border-color:rgba(var(--accent-rgb),.5); color:var(--accent); }
|
||||
|
||||
/* ── PAGE LAYOUT ── */
|
||||
.page-wrap { max-width:1080px; margin:0 auto; padding:0 40px 80px; position:relative; z-index:1; }
|
||||
|
||||
/* ── HERO ── */
|
||||
.hero { text-align:center; padding:80px 40px 60px; position:relative; z-index:1; overflow:hidden; }
|
||||
.hero-eyebrow { font-family:var(--font-mono); font-size:11px; letter-spacing:.16em; color:var(--accent); text-transform:uppercase; margin-bottom:20px; }
|
||||
.hero h1 { font-family:var(--font-head); font-weight:900; line-height:1.05; font-size:clamp(30px,5vw,56px); letter-spacing:.04em; margin-bottom:20px; }
|
||||
.hero-accent { background:linear-gradient(135deg,var(--accent) 0%,var(--accent-grad) 100%); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
|
||||
.hero-sub { font-size:clamp(14px,2vw,17px); color:var(--muted); max-width:560px; margin:0 auto 36px; font-weight:300; line-height:1.7; }
|
||||
.hero-note { font-family:var(--font-mono); font-size:11px; color:var(--muted); letter-spacing:.06em; }
|
||||
.hero-note span { color:var(--accent); }
|
||||
.hero::after {
|
||||
content:''; position:absolute; left:50%; top:40px; transform:translateX(-50%);
|
||||
width:500px; height:500px; border-radius:50%;
|
||||
background:radial-gradient(ellipse, rgba(var(--accent-rgb),.06) 0%, transparent 70%);
|
||||
pointer-events:none; z-index:-1;
|
||||
}
|
||||
|
||||
/* ── TOC (Table of Contents) ── */
|
||||
.toc-bar {
|
||||
display:flex; align-items:center; gap:8px; flex-wrap:wrap;
|
||||
padding:16px 0 32px; border-bottom:1px solid var(--border); margin-bottom:48px;
|
||||
}
|
||||
.toc-label { font-family:var(--font-mono); font-size:10px; letter-spacing:.12em; color:var(--muted); text-transform:uppercase; margin-right:4px; }
|
||||
.toc-link {
|
||||
font-family:var(--font-mono); font-size:11px; color:var(--muted); text-decoration:none;
|
||||
background:var(--surface); border:1px solid var(--border); border-radius:6px;
|
||||
padding:5px 12px; transition:all .2s; letter-spacing:.04em;
|
||||
}
|
||||
.toc-link:hover { border-color:rgba(var(--accent-rgb),.4); color:var(--accent); }
|
||||
|
||||
/* ── SECTION HEADINGS ── */
|
||||
.api-section { margin-bottom:60px; }
|
||||
.api-section-header { display:flex; align-items:center; gap:14px; margin-bottom:24px; }
|
||||
.api-section-icon {
|
||||
width:38px; height:38px; border-radius:10px; flex-shrink:0;
|
||||
background:rgba(var(--accent-rgb),.1); border:1px solid rgba(var(--accent-rgb),.2);
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
}
|
||||
.api-section-icon svg { color:var(--accent); }
|
||||
.api-section-title { font-family:var(--font-head); font-size:16px; font-weight:700; letter-spacing:.04em; }
|
||||
.api-section-desc { font-size:13px; color:var(--muted); margin-bottom:20px; line-height:1.65; }
|
||||
|
||||
/* ── ENDPOINT TABLE ── */
|
||||
.endpoint-table { width:100%; border-collapse:collapse; }
|
||||
.endpoint-table thead tr {
|
||||
border-bottom:1px solid var(--border);
|
||||
}
|
||||
.endpoint-table thead th {
|
||||
font-family:var(--font-mono); font-size:10px; letter-spacing:.12em; color:var(--muted);
|
||||
text-transform:uppercase; text-align:left; padding:10px 14px; font-weight:400;
|
||||
}
|
||||
.endpoint-table tbody tr {
|
||||
border-bottom:1px solid var(--border);
|
||||
transition:background .15s;
|
||||
}
|
||||
.endpoint-table tbody tr:last-child { border-bottom:none; }
|
||||
.endpoint-table tbody tr:hover { background:rgba(255,255,255,.025); }
|
||||
.endpoint-table td { padding:14px; vertical-align:top; }
|
||||
|
||||
/* Method badges */
|
||||
.method {
|
||||
font-family:var(--font-mono); font-size:10px; font-weight:700; letter-spacing:.08em;
|
||||
padding:3px 9px; border-radius:5px; white-space:nowrap; display:inline-block;
|
||||
}
|
||||
.method-get { background:rgba(0,200,120,.12); color:#00c87a; border:1px solid rgba(0,200,120,.25); }
|
||||
.method-post { background:rgba(var(--accent-rgb),.12); color:var(--accent); border:1px solid rgba(var(--accent-rgb),.25); }
|
||||
.method-patch { background:rgba(245,166,35,.12); color:#f5a623; border:1px solid rgba(245,166,35,.25); }
|
||||
.method-delete { background:rgba(255,94,98,.12); color:#ff5e62; border:1px solid rgba(255,94,98,.25); }
|
||||
|
||||
/* Endpoint path */
|
||||
.ep-path {
|
||||
font-family:var(--font-mono); font-size:12px; color:var(--text);
|
||||
background:var(--surface2); border:1px solid var(--border);
|
||||
border-radius:6px; padding:4px 10px; white-space:nowrap; display:inline-block;
|
||||
}
|
||||
.ep-path .ep-param { color:var(--accent3-light); }
|
||||
|
||||
/* Auth badge */
|
||||
.auth-badge {
|
||||
font-family:var(--font-mono); font-size:9px; letter-spacing:.08em;
|
||||
padding:2px 8px; border-radius:4px; white-space:nowrap; display:inline-block;
|
||||
}
|
||||
.auth-yes { background:rgba(var(--accent-rgb),.1); color:var(--accent); border:1px solid rgba(var(--accent-rgb),.2); }
|
||||
.auth-admin { background:rgba(124,106,247,.12); color:var(--accent3-light); border:1px solid rgba(124,106,247,.2); }
|
||||
.auth-none { background:var(--surface2); color:var(--muted); border:1px solid var(--border); }
|
||||
|
||||
/* Description */
|
||||
.ep-desc { font-size:13px; color:var(--muted); line-height:1.6; }
|
||||
.ep-desc code {
|
||||
font-family:var(--font-mono); font-size:11px; color:var(--text);
|
||||
background:var(--surface2); border:1px solid var(--border);
|
||||
border-radius:4px; padding:1px 6px;
|
||||
}
|
||||
.ep-desc .ep-tag { color:var(--accent3-light); font-style:italic; }
|
||||
|
||||
/* ── INTRO BOX ── */
|
||||
.intro-box {
|
||||
background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);
|
||||
padding:24px 28px; margin-bottom:48px; position:relative; overflow:hidden;
|
||||
}
|
||||
.intro-box::before {
|
||||
content:''; position:absolute; top:0; left:0; right:0; height:2px;
|
||||
background:linear-gradient(90deg,var(--accent),var(--accent-grad));
|
||||
}
|
||||
.intro-box h2 { font-family:var(--font-head); font-size:13px; font-weight:700; letter-spacing:.05em; margin-bottom:10px; }
|
||||
.intro-box p { font-size:13px; color:var(--muted); line-height:1.7; }
|
||||
.intro-box code {
|
||||
font-family:var(--font-mono); font-size:12px; color:var(--accent);
|
||||
background:rgba(var(--accent-rgb),.08); border:1px solid rgba(var(--accent-rgb),.2);
|
||||
border-radius:4px; padding:1px 7px;
|
||||
}
|
||||
|
||||
/* ── FOOTER ── */
|
||||
footer {
|
||||
border-top:1px solid var(--border); padding:32px 40px;
|
||||
display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:16px;
|
||||
position:relative; z-index:1;
|
||||
}
|
||||
.footer-logo { font-family:var(--font-head); font-size:14px; font-weight:700; color:var(--muted); letter-spacing:.04em; display:flex; align-items:center; gap:6px; }
|
||||
.footer-logo span { color:var(--accent); }
|
||||
.footer-links { display:flex; gap:20px; align-items:center; }
|
||||
.footer-links a { font-family:var(--font-mono); font-size:11px; color:var(--muted); text-decoration:none; letter-spacing:.06em; transition:color .2s; }
|
||||
.footer-links a:hover { color:var(--accent); }
|
||||
.footer-note { font-family:var(--font-mono); font-size:10px; color:var(--muted); opacity:.5; letter-spacing:.05em; }
|
||||
|
||||
/* ── REVEAL ANIMATIONS ── */
|
||||
.reveal { opacity:0; transform:translateY(20px); transition:opacity .5s ease, transform .5s ease; }
|
||||
.reveal.visible { opacity:1; transform:none; }
|
||||
.reveal-delay-1 { transition-delay:.08s; }
|
||||
.reveal-delay-2 { transition-delay:.16s; }
|
||||
.reveal-delay-3 { transition-delay:.24s; }
|
||||
|
||||
/* ── RESPONSIVE ── */
|
||||
@media(max-width:900px) {
|
||||
header { padding:0 20px; }
|
||||
.hero { padding:60px 20px 40px; }
|
||||
.page-wrap { padding:0 20px 60px; }
|
||||
.endpoint-table { display:block; overflow-x:auto; }
|
||||
footer { padding:24px 20px; }
|
||||
}
|
||||
@media(max-width:600px) {
|
||||
.toc-bar { gap:6px; }
|
||||
.ep-path { white-space:normal; word-break:break-all; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ── HEADER ── -->
|
||||
<header>
|
||||
<a class="logo" href="/">
|
||||
<span class="logo-icon-wrap"><img class="logo-icon" src="/static/qk-ico.png" alt=""><span class="logo-icon-tint"></span></span>QR<span class="logo-dot-text">knit</span>
|
||||
</a>
|
||||
<div class="header-right">
|
||||
<div class="theme-picker">
|
||||
<button class="theme-swatch" data-theme="default" title="Default" aria-label="Default blue theme"></button>
|
||||
<button class="theme-swatch" data-theme="gold" title="Gold" aria-label="Gold theme"></button>
|
||||
<button class="theme-swatch" data-theme="green" title="Green" aria-label="Green theme"></button>
|
||||
<button class="theme-swatch" data-theme="red" title="Red" aria-label="Red theme"></button>
|
||||
</div>
|
||||
<a class="btn-outline" href="/">Home</a>
|
||||
<a class="btn-primary" href="/app">Open App →</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<!-- ── HERO ── -->
|
||||
<section class="hero">
|
||||
<div class="hero-eyebrow reveal">REST API · Session Auth · JSON</div>
|
||||
<h1 class="reveal reveal-delay-1"><span class="hero-accent">API</span> Reference</h1>
|
||||
<p class="hero-sub reveal reveal-delay-2">
|
||||
Complete reference for the QRknit REST API. Manage links, generate QR codes, pull analytics, and administer users — all from your own code.
|
||||
</p>
|
||||
<div class="hero-note reveal reveal-delay-3">Base URL: <span>https://your-instance.com</span> · Format: <span>application/json</span></div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- ── MAIN CONTENT ── -->
|
||||
<div class="page-wrap">
|
||||
|
||||
<!-- Intro box -->
|
||||
<div class="intro-box reveal">
|
||||
<h2>Authentication</h2>
|
||||
<p>
|
||||
All write endpoints require an active session. Log in via the web UI or call <code>POST /api/auth/login</code> with
|
||||
<code>{"username": "…", "password": "…"}</code> — the server sets an HttpOnly session cookie valid for 30 days.
|
||||
Include that cookie in all subsequent requests. Endpoints marked <strong>Admin</strong> additionally require
|
||||
the authenticated user to have admin privileges.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- TOC -->
|
||||
<nav class="toc-bar reveal">
|
||||
<span class="toc-label">Jump to</span>
|
||||
<a class="toc-link" href="#auth">Auth</a>
|
||||
<a class="toc-link" href="#links">Links</a>
|
||||
<a class="toc-link" href="#utilities">Utilities</a>
|
||||
<a class="toc-link" href="#admin">Admin</a>
|
||||
<a class="toc-link" href="#contact">Contact</a>
|
||||
<a class="toc-link" href="#public">Public</a>
|
||||
</nav>
|
||||
|
||||
|
||||
<!-- ── AUTH ── -->
|
||||
<section class="api-section reveal" id="auth">
|
||||
<div class="api-section-header">
|
||||
<div class="api-section-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
|
||||
</div>
|
||||
<div class="api-section-title">Auth</div>
|
||||
</div>
|
||||
<p class="api-section-desc">Session-based login. A successful login sets a 30-day HttpOnly cookie used for all authenticated requests.</p>
|
||||
<table class="endpoint-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Method</th>
|
||||
<th>Endpoint</th>
|
||||
<th>Auth</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="method method-post">POST</span></td>
|
||||
<td><span class="ep-path">/api/auth/login</span></td>
|
||||
<td><span class="auth-badge auth-none">—</span></td>
|
||||
<td class="ep-desc">Login — body: <code>{"username": "…", "password": "…"}</code>. Sets session cookie and returns <code>plan</code>, <code>plan_limits</code>, and <code>org_id</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-post">POST</span></td>
|
||||
<td><span class="ep-path">/api/auth/logout</span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Clear the current session and invalidate the cookie.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/auth/me</span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Returns <code>{"authenticated": true, "username": "…", "is_admin": bool, "org_id": int|null, "plan": "…", "plan_limits": {…}}</code>.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- ── LINKS ── -->
|
||||
<section class="api-section reveal" id="links">
|
||||
<div class="api-section-header">
|
||||
<div class="api-section-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
</div>
|
||||
<div class="api-section-title">Links</div>
|
||||
</div>
|
||||
<p class="api-section-desc">Create, read, update, and delete short links. Org members see all links within their org. Admin accounts see only their own links by default, with optional <code>?user=</code> filtering.</p>
|
||||
<table class="endpoint-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Method</th>
|
||||
<th>Endpoint</th>
|
||||
<th>Auth</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="method method-post">POST</span></td>
|
||||
<td><span class="ep-path">/api/shorten</span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Create a short link. Body: <code>{"url": "…", "code": "…", "title": "…", "expires_at": "…", "tags": […], "is_pinned": bool}</code>. <code>code</code> is optional — omit for a random code.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/links</span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">List links. Query params: <code>?q=</code> (search), <code>?tag=</code>, <code>?page=</code>, <code>?per_page=</code>. Admin can add <code>?user=<username></code> to filter by user.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/links/<span class="ep-param">:code</span></span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Link detail — includes <code>created_by</code> username.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-patch">PATCH</span></td>
|
||||
<td><span class="ep-path">/api/links/<span class="ep-param">:code</span></span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Edit link — updatable fields: <code>url</code>, <code>title</code>, <code>expires_at</code>, <code>tags</code>, <code>is_pinned</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-delete">DELETE</span></td>
|
||||
<td><span class="ep-path">/api/links/<span class="ep-param">:code</span></span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Delete a link.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/links/<span class="ep-param">:code</span>/analytics</span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Click analytics. Query: <code>?days=7|30|90</code> (capped to plan's max window). Returns <code>daily</code>, <code>referrers</code>, <code>devices</code>, <code>browsers</code>, <code>countries</code>, <code>heatmap</code> (7×24 array), and <code>max_days</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/links/<span class="ep-param">:code</span>/clicks/export</span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Download raw click events as CSV. Columns: <code>timestamp</code>, <code>referrer</code>, <code>device</code>, <code>browser</code>, <code>country</code>.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- ── UTILITIES ── -->
|
||||
<section class="api-section reveal" id="utilities">
|
||||
<div class="api-section-header">
|
||||
<div class="api-section-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"></path><path d="M15.54 8.46a5 5 0 0 1 0 7.07M8.46 8.46a5 5 0 0 0 0 7.07"></path></svg>
|
||||
</div>
|
||||
<div class="api-section-title">Utilities</div>
|
||||
</div>
|
||||
<p class="api-section-desc">Stats, plan usage, tags, QR code generation, bulk operations, and CSV import/export.</p>
|
||||
<table class="endpoint-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Method</th>
|
||||
<th>Endpoint</th>
|
||||
<th>Auth</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/stats</span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Dashboard stats: total links, total clicks, clicks in last 7 days, top 5 links, and a 30-day <code>daily</code> click array.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/plan</span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Current user's effective plan, limits, and live usage — <code>{plan, limits: {max_links, monthly_clicks, analytics_days}, usage: {active_links, monthly_clicks}}</code>. Org members see pooled usage across all org members.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/tags</span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Tags scoped to the requesting user — own tags for solo users and admins; all org member tags for org members.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/fetch-title</span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Server-side page title fetch. Query: <code>?url=<url></code>. Returns <code>{"title": "…"}</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/qr/<span class="ep-param">:code</span></span></td>
|
||||
<td><span class="auth-badge auth-none">—</span></td>
|
||||
<td class="ep-desc">QR PNG for a short link. Query params: <code>?fg=</code>, <code>?bg=</code>, <code>?size=</code>, <code>?style=</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/qr/custom</span></td>
|
||||
<td><span class="auth-badge auth-none">—</span></td>
|
||||
<td class="ep-desc">QR PNG for any URL. Query: <code>?url=</code>, <code>?fg=</code>, <code>?bg=</code>, <code>?size=</code>, <code>?style=</code>. Styles: <code>square</code>, <code>rounded</code>, <code>dots</code>, <code>vertical</code>, <code>horizontal</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-post">POST</span></td>
|
||||
<td><span class="ep-path">/api/qr/custom</span></td>
|
||||
<td><span class="auth-badge auth-none">—</span></td>
|
||||
<td class="ep-desc">QR PNG with logo overlay. Body: <code>{url, fg, bg, size, style, logo}</code> where <code>logo</code> is a base64-encoded image string.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-post">POST</span></td>
|
||||
<td><span class="ep-path">/api/links/bulk</span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Bulk operations. Body: <code>{"action": "delete"|"tag"|"expire", "codes": […]}</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/links/export</span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Download all links as CSV.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-post">POST</span></td>
|
||||
<td><span class="ep-path">/api/links/import</span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Import links from CSV. Body: <code>{"csv": "…"}</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/health</span></td>
|
||||
<td><span class="auth-badge auth-none">—</span></td>
|
||||
<td class="ep-desc">Health check — returns <code>{"status": "ok"}</code>. Used by Docker and uptime monitors.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/config</span></td>
|
||||
<td><span class="auth-badge auth-none">—</span></td>
|
||||
<td class="ep-desc">Public deployment config — returns non-sensitive values like <code>app_name</code> and <code>base_url</code>.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- ── ADMIN ── -->
|
||||
<section class="api-section reveal" id="admin">
|
||||
<div class="api-section-header">
|
||||
<div class="api-section-icon" style="background:rgba(var(--accent3-rgb),.1);border-color:rgba(var(--accent3-rgb),.2)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent3-light)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
|
||||
</div>
|
||||
<div class="api-section-title" style="color:var(--accent3-light)">Admin</div>
|
||||
</div>
|
||||
<p class="api-section-desc">All Admin endpoints require an authenticated admin session. These manage users, organizations, and the message inbox.</p>
|
||||
<table class="endpoint-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Method</th>
|
||||
<th>Endpoint</th>
|
||||
<th>Auth</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/admin/users</span></td>
|
||||
<td><span class="auth-badge auth-admin">Admin</span></td>
|
||||
<td class="ep-desc">List all users with link counts, plan, org name, and org plan.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-post">POST</span></td>
|
||||
<td><span class="ep-path">/api/admin/users</span></td>
|
||||
<td><span class="auth-badge auth-admin">Admin</span></td>
|
||||
<td class="ep-desc">Create user. Body: <code>{"username": "…", "password": "…", "is_admin": bool, "plan": "…"}</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-patch">PATCH</span></td>
|
||||
<td><span class="ep-path">/api/admin/users/<span class="ep-param">:id</span></span></td>
|
||||
<td><span class="auth-badge auth-admin">Admin</span></td>
|
||||
<td class="ep-desc">Update plan, admin status, and/or org. Body: <code>{plan?, is_admin?, org_id?}</code>. Set <code>org_id: null</code> to remove from org.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-delete">DELETE</span></td>
|
||||
<td><span class="ep-path">/api/admin/users/<span class="ep-param">:id</span></span></td>
|
||||
<td><span class="auth-badge auth-admin">Admin</span></td>
|
||||
<td class="ep-desc">Delete user. Cannot delete yourself.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-patch">PATCH</span></td>
|
||||
<td><span class="ep-path">/api/admin/users/<span class="ep-param">:id</span>/password</span></td>
|
||||
<td><span class="auth-badge auth-admin">Admin</span></td>
|
||||
<td class="ep-desc">Change user password. Body: <code>{"password": "…"}</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/admin/organizations</span></td>
|
||||
<td><span class="auth-badge auth-admin">Admin</span></td>
|
||||
<td class="ep-desc">List all organizations with member counts and plan.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-post">POST</span></td>
|
||||
<td><span class="ep-path">/api/admin/organizations</span></td>
|
||||
<td><span class="auth-badge auth-admin">Admin</span></td>
|
||||
<td class="ep-desc">Create organization. Body: <code>{"name": "…", "plan": "…"}</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-patch">PATCH</span></td>
|
||||
<td><span class="ep-path">/api/admin/organizations/<span class="ep-param">:id</span></span></td>
|
||||
<td><span class="auth-badge auth-admin">Admin</span></td>
|
||||
<td class="ep-desc">Update organization name and/or plan. Body: <code>{name?, plan?}</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-delete">DELETE</span></td>
|
||||
<td><span class="ep-path">/api/admin/organizations/<span class="ep-param">:id</span></span></td>
|
||||
<td><span class="auth-badge auth-admin">Admin</span></td>
|
||||
<td class="ep-desc">Delete organization. Members are unassigned but not deleted.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/admin/messages</span></td>
|
||||
<td><span class="auth-badge auth-admin">Admin</span></td>
|
||||
<td class="ep-desc">List all contact/portal messages, newest first.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-delete">DELETE</span></td>
|
||||
<td><span class="ep-path">/api/admin/messages/<span class="ep-param">:id</span></span></td>
|
||||
<td><span class="auth-badge auth-admin">Admin</span></td>
|
||||
<td class="ep-desc">Delete a message.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-patch">PATCH</span></td>
|
||||
<td><span class="ep-path">/api/admin/messages/<span class="ep-param">:id</span>/read</span></td>
|
||||
<td><span class="auth-badge auth-admin">Admin</span></td>
|
||||
<td class="ep-desc">Mark a message as read.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- ── CONTACT ── -->
|
||||
<section class="api-section reveal" id="contact">
|
||||
<div class="api-section-header">
|
||||
<div class="api-section-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
|
||||
</div>
|
||||
<div class="api-section-title">Contact</div>
|
||||
</div>
|
||||
<p class="api-section-desc">Public endpoint for submitting contact and support messages. No authentication required.</p>
|
||||
<table class="endpoint-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Method</th>
|
||||
<th>Endpoint</th>
|
||||
<th>Auth</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="method method-post">POST</span></td>
|
||||
<td><span class="ep-path">/api/contact</span></td>
|
||||
<td><span class="auth-badge auth-none">—</span></td>
|
||||
<td class="ep-desc">Submit a contact message. Body: <code>{"name": "…", "email": "…", "subject": "…", "body": "…"}</code>.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- ── PUBLIC ── -->
|
||||
<section class="api-section reveal" id="public">
|
||||
<div class="api-section-header">
|
||||
<div class="api-section-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
|
||||
</div>
|
||||
<div class="api-section-title">Public Redirects</div>
|
||||
</div>
|
||||
<p class="api-section-desc">Short link redirection and SEO endpoints. No authentication required.</p>
|
||||
<table class="endpoint-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Method</th>
|
||||
<th>Endpoint</th>
|
||||
<th>Auth</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/<span class="ep-param">:code</span></span></td>
|
||||
<td><span class="auth-badge auth-none">—</span></td>
|
||||
<td class="ep-desc">Redirect to the destination URL for a short code. Records click analytics (referrer, device, browser, country).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/robots.txt</span></td>
|
||||
<td><span class="auth-badge auth-none">—</span></td>
|
||||
<td class="ep-desc">Standard robots.txt for crawler directives.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/sitemap.xml</span></td>
|
||||
<td><span class="auth-badge auth-none">—</span></td>
|
||||
<td class="ep-desc">Auto-generated XML sitemap.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
</div><!-- /page-wrap -->
|
||||
|
||||
|
||||
<!-- ── FOOTER ── -->
|
||||
<footer>
|
||||
<div class="footer-logo">
|
||||
<span class="logo-icon-wrap" style="isolation:isolate;position:relative;display:inline-flex"><img style="height:20px;width:20px;object-fit:contain;border-radius:2px;display:block" src="/static/qk-ico.png" alt=""><span style="position:absolute;inset:0;background:var(--accent);mix-blend-mode:hue;border-radius:2px;pointer-events:none"></span></span>
|
||||
QR<span>knit</span>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<a href="/">Home</a>
|
||||
<a href="/api-docs">API Docs</a>
|
||||
<a href="/app">App</a>
|
||||
</div>
|
||||
<div class="footer-note">© 2025 QRknit · All rights reserved · <a href="/notices" style="color:inherit;text-decoration:none;opacity:.7">Third-party notices</a></div>
|
||||
</footer>
|
||||
|
||||
|
||||
<script>
|
||||
// ── Theme picker ──────────────────────────────────────────────────────────
|
||||
function applyTheme(name) {
|
||||
if (name && name !== 'default') {
|
||||
document.documentElement.setAttribute('data-theme', name);
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
}
|
||||
localStorage.setItem('qrknit-theme', name || 'default');
|
||||
document.querySelectorAll('.theme-swatch').forEach(s => {
|
||||
s.classList.toggle('active', s.dataset.theme === (name || 'default'));
|
||||
});
|
||||
}
|
||||
document.querySelectorAll('.theme-swatch').forEach(s => {
|
||||
s.addEventListener('click', () => applyTheme(s.dataset.theme));
|
||||
});
|
||||
(function(){
|
||||
const t = localStorage.getItem('qrknit-theme') || 'default';
|
||||
document.querySelectorAll('.theme-swatch').forEach(s => {
|
||||
s.classList.toggle('active', s.dataset.theme === t);
|
||||
});
|
||||
})();
|
||||
|
||||
// ── Scroll reveal ─────────────────────────────────────────────────────────
|
||||
(function(){
|
||||
const els = document.querySelectorAll('.reveal');
|
||||
if (!els.length) return;
|
||||
const io = new IntersectionObserver((entries) => {
|
||||
entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('visible'); io.unobserve(e.target); } });
|
||||
}, { threshold: 0.08 });
|
||||
els.forEach(el => io.observe(el));
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user