Compare commits

..

6 Commits

Author SHA1 Message Date
Zin Bo Thit
ad1e960e71 Fix gateway SMS API routes
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-08-03 15:36:38 +06:30
Zin Bo Thit
3eaded1033 Queue OTP SMS messages for delivery
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-08-03 14:58:20 +06:30
Zin Bo Thit
6678a5b919 Merge branch 'main' of https://git.qikbite.asia/minzeyaphyo/MySMSAPio into main
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-08-03 14:38:05 +06:30
Zin Bo Thit
ce49a72e01 Fix OTP API routes 2026-08-03 14:37:51 +06:30
Min Zeya Phyo
a5ade4006d welcome page and documentatino
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-08-01 00:27:37 +08:00
Min Zeya Phyo
798162ae4f welcome page and documentatino 2026-08-01 00:27:32 +08:00
6 changed files with 1194 additions and 11 deletions

View File

@@ -0,0 +1,15 @@
class PagesController < ActionController::Base
layout "public"
protect_from_forgery with: :exception
# GET /
# Public landing page introducing the SMS Gateway API.
def home
end
# GET /docs
# Interactive API documentation covering every endpoint, the WebSocket
# protocol, webhooks, authentication, rate limits, and error codes.
def docs
end
end

View File

@@ -54,7 +54,8 @@ class OtpCode < ApplicationRecord
sms = SmsMessage.create!(
direction: "outbound",
phone_number: normalized_phone,
message_body: "Your OTP code is: #{otp.code}. Valid for #{expiry_minutes} minutes. Do not share this code."
message_body: "Your OTP code is: #{otp.code}. Valid for #{expiry_minutes} minutes. Do not share this code.",
status: "queued"
)
{ otp: otp, sms: sms }

View File

@@ -0,0 +1,273 @@
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title><%= content_for(:title) || "MySMSAPio — SMS Gateway API" %></title>
<meta name="description" content="A Rails 8 SMS Gateway Backend API for managing SMS messaging through Android gateway devices. Send SMS, manage OTPs, and communicate with gateway devices in real-time.">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
<link rel="icon" href="/icon.png" type="image/png">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/icon.png">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<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=Syne:wght@600;700;800&family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--accent: #38bdf8;
--accent-deep: #0ea5e9;
--accent-glow: rgba(56, 189, 248, 0.45);
}
.font-display { font-family: 'Syne', sans-serif; }
.font-body { font-family: 'Outfit', sans-serif; }
.font-mono-tech { font-family: 'JetBrains Mono', ui-monospace, monospace; }
body { font-family: 'Outfit', sans-serif; }
/* Immersive dark stage */
.stage {
position: relative;
background: radial-gradient(ellipse 90% 70% at 50% -10%, #0b1220 0%, #050912 55%, #000 100%);
overflow: hidden;
}
.stage::before {
content: "";
position: absolute; inset: 0;
background-image:
linear-gradient(rgba(56, 189, 248, 0.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(56, 189, 248, 0.06) 1px, transparent 1px);
background-size: 48px 48px;
-webkit-mask-image: radial-gradient(ellipse at top, black 10%, transparent 70%);
mask-image: radial-gradient(ellipse at top, black 10%, transparent 70%);
pointer-events: none;
}
.stage::after {
content: "";
position: absolute; inset: 0;
background:
radial-gradient(circle at 12% 14%, rgba(56, 189, 248, 0.13), transparent 42%),
radial-gradient(circle at 88% 8%, rgba(14, 165, 233, 0.10), transparent 45%),
radial-gradient(circle at 70% 90%, rgba(56, 189, 248, 0.07), transparent 40%);
pointer-events: none;
}
.glass {
background: rgba(255, 255, 255, 0.035);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.glass-strong {
background: rgba(15, 23, 42, 0.55);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border: 1px solid rgba(148, 163, 184, 0.14);
}
.beacon-ring { animation: beacon-pulse 2.6s ease-out infinite; }
.beacon-ring.delay-1 { animation-delay: 0.9s; }
.beacon-ring.delay-2 { animation-delay: 1.8s; }
@keyframes beacon-pulse {
0% { transform: scale(0.6); opacity: 0.55; }
80% { opacity: 0; }
100% { transform: scale(1.9); opacity: 0; }
}
.btn-accent {
background: linear-gradient(135deg, #38bdf8 0%, #0ea5e9 100%);
box-shadow: 0 10px 30px -10px var(--accent-glow), inset 0 1px 0 rgba(255,255,255,0.25);
transition: transform .15s ease, box-shadow .2s ease, filter .2s ease;
}
.btn-accent:hover { transform: translateY(-1px); filter: brightness(1.08); box-shadow: 0 16px 40px -10px var(--accent-glow), inset 0 1px 0 rgba(255,255,255,0.3); }
.btn-accent:active { transform: translateY(0); }
.btn-ghost {
background: rgba(255,255,255,0.04);
border: 1px solid rgba(148, 163, 184, 0.22);
transition: border-color .18s ease, background .18s ease, transform .15s ease;
}
.btn-ghost:hover { border-color: var(--accent); background: rgba(56, 189, 248, 0.10); transform: translateY(-1px); }
.nav-link {
position: relative;
transition: color .18s ease;
}
.nav-link::after {
content: "";
position: absolute; left: 0; bottom: -4px;
height: 2px; width: 0;
background: var(--accent);
transition: width .22s ease;
}
.nav-link:hover::after, .nav-link.is-active::after { width: 100%; }
.nav-link.is-active { color: #fff; }
@keyframes fade-up {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-up { animation: fade-up .6s cubic-bezier(0.16, 1, 0.3, 1) both; }
.fade-up.d-1 { animation-delay: .08s; }
.fade-up.d-2 { animation-delay: .16s; }
.fade-up.d-3 { animation-delay: .24s; }
.fade-up.d-4 { animation-delay: .32s; }
.fade-up.d-5 { animation-delay: .40s; }
/* Code blocks */
.code-block {
background: rgba(2, 6, 23, 0.7);
border: 1px solid rgba(148, 163, 184, 0.16);
font-family: 'JetBrains Mono', ui-monospace, monospace;
}
.code-block .tok-key { color: #7dd3fc; }
.code-block .tok-str { color: #86efac; }
.code-block .tok-num { color: #fcd34d; }
.code-block .tok-com { color: #64748b; font-style: italic; }
.code-block .tok-punc { color: #94a3b8; }
.code-block .tok-bool { color: #f0abfc; }
.copy-btn {
transition: color .15s ease, background .15s ease;
}
.copy-btn.copied { color: #4ade80; }
/* Docs sidebar */
.docs-sidebar a {
transition: color .15s ease, background .15s ease;
}
.docs-sidebar a:hover { color: #e2e8f0; }
.docs-sidebar a.is-active {
color: var(--accent);
background: rgba(56, 189, 248, 0.08);
border-left-color: var(--accent);
}
/* Method badges */
.badge-get { background: rgba(34, 197, 94, 0.14); color: #4ade80; border-color: rgba(74, 222, 128, 0.3); }
.badge-post { background: rgba(56, 189, 248, 0.14); color: #38bdf8; border-color: rgba(56, 189, 248, 0.3); }
.badge-delete { background: rgba(244, 63, 94, 0.14); color: #fb7185; border-color: rgba(251, 113, 133, 0.3); }
.badge-ws { background: rgba(168, 85, 247, 0.14); color: #c084fc; border-color: rgba(192, 132, 252, 0.3); }
.gradient-text {
background: linear-gradient(120deg, #e2e8f0 0%, #38bdf8 60%, #0ea5e9 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.scroll-fade {
mask-image: linear-gradient(to bottom, black 80%, transparent 100%);
}
html { scroll-behavior: smooth; scroll-padding-top: 90px; }
@media (prefers-reduced-motion: reduce) {
.beacon-ring, .fade-up { animation: none; }
html { scroll-behavior: auto; }
}
</style>
</head>
<body class="stage min-h-full text-slate-200">
<!-- Top navigation -->
<header class="sticky top-0 z-40 glass-strong border-b border-white/5">
<nav class="mx-auto flex max-w-7xl items-center justify-between px-5 py-4 sm:px-8">
<%= link_to root_path, class: "flex items-center gap-3" do %>
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-sky-400 to-sky-600 shadow-lg shadow-sky-500/30">
<i class="fas fa-sms text-lg text-slate-950"></i>
</div>
<div class="leading-none">
<span class="font-display text-lg font-extrabold tracking-tight text-white">MySMSAPio</span>
<span class="font-mono-tech ml-2 text-[10px] uppercase tracking-[0.3em] text-sky-400/80">v1</span>
</div>
<% end %>
<div class="hidden items-center gap-8 sm:flex">
<%= link_to "Home", root_path, class: "nav-link text-sm font-medium text-slate-300 #{current_page?(root_path) ? 'is-active' : ''}" %>
<%= link_to "API Docs", docs_path, class: "nav-link text-sm font-medium text-slate-300 #{current_page?(docs_path) ? 'is-active' : ''}" %>
<%= link_to "Admin Console", admin_login_path, class: "nav-link text-sm font-medium text-slate-300" %>
</div>
<div class="flex items-center gap-3">
<%= link_to docs_path, class: "btn-accent hidden items-center gap-2 rounded-lg px-4 py-2 text-sm font-semibold text-slate-950 sm:inline-flex" do %>
<i class="fas fa-book"></i> Read the Docs
<% end %>
<a href="https://github.com" target="_blank" rel="noopener" class="btn-ghost inline-flex h-9 w-9 items-center justify-center rounded-lg text-slate-300" aria-label="GitHub">
<i class="fab fa-github"></i>
</a>
</div>
</nav>
</header>
<main class="relative">
<%= yield %>
</main>
<!-- Footer -->
<footer class="relative border-t border-white/5 bg-black/30">
<div class="mx-auto max-w-7xl px-5 py-12 sm:px-8">
<div class="flex flex-col items-center justify-between gap-6 sm:flex-row">
<div class="flex items-center gap-3">
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-sky-400 to-sky-600">
<i class="fas fa-sms text-sm text-slate-950"></i>
</div>
<div>
<p class="font-display text-sm font-bold text-white">MySMSAPio</p>
<p class="font-mono-tech text-[10px] uppercase tracking-[0.2em] text-slate-500">SMS Gateway API</p>
</div>
</div>
<div class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-sm text-slate-400">
<%= link_to "Home", root_path, class: "hover:text-sky-400 transition-colors" %>
<%= link_to "API Docs", docs_path, class: "hover:text-sky-400 transition-colors" %>
<%= link_to "Admin", admin_login_path, class: "hover:text-sky-400 transition-colors" %>
<span class="text-slate-600">·</span>
<span class="font-mono-tech text-xs">Rails 8 · Ruby 3.4</span>
</div>
</div>
<p class="mt-8 text-center text-xs text-slate-600">© <%= Time.current.year %> MySMSAPio. Built with Rails 8, Sidekiq, Action Cable & Redis.</p>
</div>
</footer>
<script>
// Copy-to-clipboard for any element with [data-copy]
document.addEventListener("click", (event) => {
const btn = event.target.closest("[data-copy]");
if (!btn) return;
const text = btn.getAttribute("data-copy");
navigator.clipboard.writeText(text).then(() => {
const original = btn.innerHTML;
btn.classList.add("copied");
btn.innerHTML = '<i class="fas fa-check"></i>';
setTimeout(() => { btn.classList.remove("copied"); btn.innerHTML = original; }, 1400);
});
});
// Docs sidebar active link on scroll
(function () {
const links = document.querySelectorAll(".docs-sidebar a[data-target]");
if (!links.length) return;
const sections = Array.from(links).map(l => document.getElementById(l.dataset.target)).filter(Boolean);
function setActive() {
const offset = 120;
let current = sections[0]?.id;
for (const s of sections) {
if (s.getBoundingClientRect().top - offset <= 0) current = s.id;
}
links.forEach(l => l.classList.toggle("is-active", l.dataset.target === current));
}
window.addEventListener("scroll", setActive, { passive: true });
setActive();
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,655 @@
<!-- Docs layout: sidebar + content -->
<div class="mx-auto flex max-w-7xl gap-10 px-5 py-12 sm:px-8">
<!-- ============ SIDEBAR ============ -->
<aside class="docs-sidebar hidden w-64 shrink-0 lg:block">
<div class="sticky top-24">
<p class="font-mono-tech px-3 text-[10px] uppercase tracking-[0.25em] text-slate-500">On this page</p>
<nav class="mt-4 flex flex-col gap-0.5 border-l border-white/10 text-sm">
<a href="#getting-started" data-target="getting-started" class="-ml-px border-l-2 border-transparent px-3 py-1.5 text-slate-400">Getting Started</a>
<a href="#authentication" data-target="authentication" class="-ml-px border-l-2 border-transparent px-3 py-1.5 text-slate-400">Authentication</a>
<a href="#rate-limits" data-target="rate-limits" class="-ml-px border-l-2 border-transparent px-3 py-1.5 text-slate-400">Rate Limits</a>
<a href="#errors" data-target="errors" class="-ml-px border-l-2 border-transparent px-3 py-1.5 text-slate-400">Error Codes</a>
<p class="font-mono-tech mt-5 px-3 text-[10px] uppercase tracking-[0.25em] text-slate-500">Client API</p>
<a href="#send-sms" data-target="send-sms" class="-ml-px border-l-2 border-transparent px-3 py-1.5 text-slate-400">Send SMS</a>
<a href="#sms-status" data-target="sms-status" class="-ml-px border-l-2 border-transparent px-3 py-1.5 text-slate-400">Check SMS Status</a>
<a href="#received-sms" data-target="received-sms" class="-ml-px border-l-2 border-transparent px-3 py-1.5 text-slate-400">List Received SMS</a>
<a href="#otp-send" data-target="otp-send" class="-ml-px border-l-2 border-transparent px-3 py-1.5 text-slate-400">Send OTP</a>
<a href="#otp-verify" data-target="otp-verify" class="-ml-px border-l-2 border-transparent px-3 py-1.5 text-slate-400">Verify OTP</a>
<a href="#admin-stats" data-target="admin-stats" class="-ml-px border-l-2 border-transparent px-3 py-1.5 text-slate-400">Admin Stats</a>
<a href="#admin-gateways" data-target="admin-gateways" class="-ml-px border-l-2 border-transparent px-3 py-1.5 text-slate-400">Admin Gateways</a>
<p class="font-mono-tech mt-5 px-3 text-[10px] uppercase tracking-[0.25em] text-slate-500">Gateway API</p>
<a href="#gw-register" data-target="gw-register" class="-ml-px border-l-2 border-transparent px-3 py-1.5 text-slate-400">Register Gateway</a>
<a href="#gw-heartbeat" data-target="gw-heartbeat" class="-ml-px border-l-2 border-transparent px-3 py-1.5 text-slate-400">Heartbeat</a>
<a href="#gw-sms-received" data-target="gw-sms-received" class="-ml-px border-l-2 border-transparent px-3 py-1.5 text-slate-400">Report Inbound SMS</a>
<a href="#gw-sms-status" data-target="gw-sms-status" class="-ml-px border-l-2 border-transparent px-3 py-1.5 text-slate-400">Report Delivery</a>
<p class="font-mono-tech mt-5 px-3 text-[10px] uppercase tracking-[0.25em] text-slate-500">Realtime</p>
<a href="#websocket" data-target="websocket" class="-ml-px border-l-2 border-transparent px-3 py-1.5 text-slate-400">WebSocket Channel</a>
<a href="#webhooks" data-target="webhooks" class="-ml-px border-l-2 border-transparent px-3 py-1.5 text-slate-400">Webhooks</a>
</nav>
</div>
</aside>
<!-- ============ CONTENT ============ -->
<div class="min-w-0 flex-1 space-y-20">
<!-- Hero -->
<section id="top">
<p class="font-mono-tech text-[11px] uppercase tracking-[0.3em] text-sky-400/80">Reference</p>
<h1 class="font-display mt-3 text-5xl font-extrabold tracking-tight text-white">API Documentation</h1>
<p class="mt-4 max-w-2xl text-lg text-slate-400">Every endpoint, parameter, and response for the MySMSAPio SMS Gateway. All routes are prefixed with <code class="rounded bg-white/5 px-2 py-0.5 font-mono-tech text-sm text-sky-300">/api/v1</code>.</p>
</section>
<!-- ============ GETTING STARTED ============ -->
<section id="getting-started" class="scroll-mt-24">
<h2 class="font-display text-3xl font-bold text-white">Getting Started</h2>
<div class="mt-6 space-y-4 text-slate-300">
<p>The MySMSAPio API is a JSON REST API. There are two classes of consumer:</p>
<ul class="ml-5 list-disc space-y-2 text-slate-400">
<li><span class="font-semibold text-slate-200">Client applications</span> — send SMS, generate/verify OTPs, read inbound messages, and query admin stats. Authenticated with a <code class="font-mono-tech text-sky-300">api_live_…</code> key.</li>
<li><span class="font-semibold text-slate-200">Gateway devices</span> — Android phones that register, send heartbeats, report inbound SMS and delivery receipts. Authenticated with a <code class="font-mono-tech text-sky-300">gw_live_…</code> key.</li>
</ul>
</div>
<div class="mt-6 grid gap-4 sm:grid-cols-2">
<div class="rounded-xl glass p-5">
<p class="font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Base URL</p>
<code class="mt-1 block font-mono-tech text-sm text-sky-300">https://your-host/api/v1</code>
</div>
<div class="rounded-xl glass p-5">
<p class="font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Content-Type</p>
<code class="mt-1 block font-mono-tech text-sm text-sky-300">application/json</code>
</div>
</div>
</section>
<!-- ============ AUTHENTICATION ============ -->
<section id="authentication" class="scroll-mt-24">
<h2 class="font-display text-3xl font-bold text-white">Authentication</h2>
<p class="mt-4 text-slate-400">All authenticated endpoints expect an API key in the <code class="font-mono-tech text-sky-300">Authorization</code> header as a Bearer token. Keys are SHA-256 hashed at rest — the raw key is only shown once at creation.</p>
<div class="mt-6 grid gap-4 sm:grid-cols-2">
<div class="rounded-xl glass p-5">
<div class="flex items-center gap-2">
<i class="fas fa-key text-emerald-400"></i>
<span class="font-semibold text-white">Client key</span>
</div>
<code class="mt-2 block font-mono-tech text-xs text-emerald-300">api_live_&lt;64 hex chars&gt;</code>
<p class="mt-2 text-xs text-slate-400">Permissions-based (send_sms, receive_sms…). Can expire.</p>
</div>
<div class="rounded-xl glass p-5">
<div class="flex items-center gap-2">
<i class="fas fa-mobile-screen text-sky-400"></i>
<span class="font-semibold text-white">Gateway key</span>
</div>
<code class="mt-2 block font-mono-tech text-xs text-sky-300">gw_live_&lt;64 hex chars&gt;</code>
<p class="mt-2 text-xs text-slate-400">Bound to a single gateway device. Must be active.</p>
</div>
</div>
<div class="code-block mt-6 rounded-xl p-4 text-xs leading-relaxed">
<span class="tok-com"># Bearer token in the Authorization header</span><br>
<span class="tok-key">curl</span> https://your-host/api/v1/sms/received \<br>
&nbsp;&nbsp;-H <span class="tok-str">"Authorization: Bearer api_live_..."</span>
</div>
</section>
<!-- ============ RATE LIMITS ============ -->
<section id="rate-limits" class="scroll-mt-24">
<h2 class="font-display text-3xl font-bold text-white">Rate Limits</h2>
<p class="mt-4 text-slate-400">Limits are enforced via Redis and return <code class="font-mono-tech text-sky-300">429 Too Many Requests</code> with a <code class="font-mono-tech text-sky-300">retry_after</code> hint.</p>
<div class="mt-6 overflow-hidden rounded-xl glass">
<table class="min-w-full divide-y divide-white/10 text-sm">
<thead class="bg-white/5">
<tr>
<th class="px-5 py-3 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Resource</th>
<th class="px-5 py-3 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Limit</th>
<th class="px-5 py-3 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Window</th>
</tr>
</thead>
<tbody class="divide-y divide-white/5">
<tr><td class="px-5 py-3 text-slate-300">Send SMS</td><td class="px-5 py-3 text-sky-300">100</td><td class="px-5 py-3 text-slate-400">per minute / API key</td></tr>
<tr><td class="px-5 py-3 text-slate-300">Send OTP</td><td class="px-5 py-3 text-sky-300">3</td><td class="px-5 py-3 text-slate-400">per hour / phone number</td></tr>
<tr><td class="px-5 py-3 text-slate-300">OTP verify attempts</td><td class="px-5 py-3 text-sky-300">3</td><td class="px-5 py-3 text-slate-400">per code</td></tr>
</tbody>
</table>
</div>
</section>
<!-- ============ ERRORS ============ -->
<section id="errors" class="scroll-mt-24">
<h2 class="font-display text-3xl font-bold text-white">Error Codes</h2>
<p class="mt-4 text-slate-400">Errors are returned as JSON with an <code class="font-mono-tech text-sky-300">error</code> field. Validation errors also include <code class="font-mono-tech text-sky-300">details</code>.</p>
<div class="mt-6 overflow-hidden rounded-xl glass">
<table class="min-w-full divide-y divide-white/10 text-sm">
<thead class="bg-white/5">
<tr>
<th class="px-5 py-3 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Status</th>
<th class="px-5 py-3 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Meaning</th>
</tr>
</thead>
<tbody class="divide-y divide-white/5">
<% errors = [
["400", "Bad Request — missing required parameter"],
["401", "Unauthorized — missing or invalid API key"],
["403", "Forbidden — insufficient permissions"],
["404", "Not Found — resource does not exist"],
["409", "Conflict — e.g. gateway already registered"],
["422", "Unprocessable Entity — validation failure"],
["429", "Too Many Requests — rate limit exceeded"],
["500", "Internal Server Error"]
] %>
<% errors.each do |code, meaning| %>
<tr>
<td class="px-5 py-3"><span class="rounded-md bg-red-500/10 px-2 py-0.5 font-mono-tech text-xs text-red-300"><%= code %></span></td>
<td class="px-5 py-3 text-slate-400"><%= meaning %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
</section>
<!-- ===================== CLIENT API DIVIDER ===================== -->
<div class="flex items-center gap-4">
<span class="h-px flex-1 bg-gradient-to-r from-transparent to-white/15"></span>
<span class="font-mono-tech text-[11px] uppercase tracking-[0.3em] text-sky-400/80">Client Application API</span>
<span class="h-px flex-1 bg-gradient-to-l from-transparent to-white/15"></span>
</div>
<!-- ============ SEND SMS ============ -->
<%# Reusable endpoint block. Data derived from the controller source. %>
<section id="send-sms" class="scroll-mt-24">
<div class="flex flex-wrap items-center gap-3">
<span class="rounded-md border px-2.5 py-1 font-mono-tech text-xs font-semibold badge-post">POST</span>
<code class="font-mono-tech text-lg text-white">/sms/send</code>
</div>
<h3 class="font-display mt-3 text-2xl font-bold text-white">Send an SMS</h3>
<p class="mt-2 text-slate-400">Queues an outbound message. The API returns immediately with a <code class="font-mono-tech text-sky-300">message_id</code>; delivery happens asynchronously via an available gateway. Rate limited to 100/min per API key.</p>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Request body</p>
<div class="mt-2 overflow-hidden rounded-xl glass">
<table class="min-w-full divide-y divide-white/10 text-sm">
<thead class="bg-white/5"><tr>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Parameter</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Type</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Required</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Description</th>
</tr></thead>
<tbody class="divide-y divide-white/5">
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">to</td><td class="px-5 py-2 text-slate-400">string</td><td class="px-5 py-2 text-red-300">yes</td><td class="px-5 py-2 text-slate-400">E.164 phone number (validated via Phonelib)</td></tr>
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">message</td><td class="px-5 py-2 text-slate-400">string</td><td class="px-5 py-2 text-red-300">yes</td><td class="px-5 py-2 text-slate-400">Message body (max 1600 chars)</td></tr>
</tbody>
</table>
</div>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Example request</p>
<div class="code-block mt-2 rounded-xl p-4 text-xs leading-relaxed">
<span class="tok-key">curl</span> -X POST https://your-host/api/v1/sms/send \<br>
&nbsp;&nbsp;-H <span class="tok-str">"Authorization: Bearer api_live_..."</span> \<br>
&nbsp;&nbsp;-H <span class="tok-str">"Content-Type: application/json"</span> \<br>
&nbsp;&nbsp;-d <span class="tok-str">'{"to":"+14155551234","message":"Your code is 482915"}'</span>
</div>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Response · 202 Accepted</p>
<div class="code-block mt-2 rounded-xl p-4 text-xs leading-relaxed">
{<br>
&nbsp;&nbsp;<span class="tok-key">"success"</span>: <span class="tok-bool">true</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"message_id"</span>: <span class="tok-str">"msg_3f8a9c1b2e7d4f60"</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"status"</span>: <span class="tok-str">"queued"</span><br>
}
</div>
</section>
<!-- ============ SMS STATUS ============ -->
<section id="sms-status" class="scroll-mt-24">
<div class="flex flex-wrap items-center gap-3">
<span class="rounded-md border px-2.5 py-1 font-mono-tech text-xs font-semibold badge-get">GET</span>
<code class="font-mono-tech text-lg text-white">/sms/status/:message_id</code>
</div>
<h3 class="font-display mt-3 text-2xl font-bold text-white">Check SMS Status</h3>
<p class="mt-2 text-slate-400">Returns the current lifecycle state of a message, including timestamps.</p>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Path parameters</p>
<div class="mt-2 overflow-hidden rounded-xl glass">
<table class="min-w-full divide-y divide-white/10 text-sm">
<thead class="bg-white/5"><tr>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Parameter</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Type</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Description</th>
</tr></thead>
<tbody class="divide-y divide-white/5">
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">message_id</td><td class="px-5 py-2 text-slate-400">string</td><td class="px-5 py-2 text-slate-400">The ID returned when the message was created</td></tr>
</tbody>
</table>
</div>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Response · 200 OK</p>
<div class="code-block mt-2 rounded-xl p-4 text-xs leading-relaxed">
{<br>
&nbsp;&nbsp;<span class="tok-key">"message_id"</span>: <span class="tok-str">"msg_3f8a9c1b2e7d4f60"</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"status"</span>: <span class="tok-str">"delivered"</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"sent_at"</span>: <span class="tok-str">"2026-08-01T12:00:00Z"</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"delivered_at"</span>: <span class="tok-str">"2026-08-01T12:00:03Z"</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"failed_at"</span>: <span class="tok-bool">null</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"error_message"</span>: <span class="tok-bool">null</span><br>
}
</div>
<p class="mt-3 text-xs text-slate-500">Statuses: <code class="font-mono-tech text-sky-300">queued</code> → <code class="font-mono-tech text-sky-300">pending</code> → <code class="font-mono-tech text-sky-300">sent</code> → <code class="font-mono-tech text-sky-300">delivered</code> | <code class="font-mono-tech text-sky-300">failed</code></p>
</section>
<!-- ============ RECEIVED SMS ============ -->
<section id="received-sms" class="scroll-mt-24">
<div class="flex flex-wrap items-center gap-3">
<span class="rounded-md border px-2.5 py-1 font-mono-tech text-xs font-semibold badge-get">GET</span>
<code class="font-mono-tech text-lg text-white">/sms/received</code>
</div>
<h3 class="font-display mt-3 text-2xl font-bold text-white">List Received SMS</h3>
<p class="mt-2 text-slate-400">Paginated list of inbound messages, newest first. Optional filters by phone number or date.</p>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Query parameters</p>
<div class="mt-2 overflow-hidden rounded-xl glass">
<table class="min-w-full divide-y divide-white/10 text-sm">
<thead class="bg-white/5"><tr>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Parameter</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Type</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Default</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Description</th>
</tr></thead>
<tbody class="divide-y divide-white/5">
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">phone_number</td><td class="px-5 py-2 text-slate-400">string</td><td class="px-5 py-2 text-slate-500">—</td><td class="px-5 py-2 text-slate-400">Filter by sender phone</td></tr>
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">since</td><td class="px-5 py-2 text-slate-400">datetime</td><td class="px-5 py-2 text-slate-500">—</td><td class="px-5 py-2 text-slate-400">Only messages after this time</td></tr>
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">limit</td><td class="px-5 py-2 text-slate-400">integer</td><td class="px-5 py-2 text-slate-500">50</td><td class="px-5 py-2 text-slate-400">Items per page</td></tr>
</tbody>
</table>
</div>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Response · 200 OK</p>
<div class="code-block mt-2 rounded-xl p-4 text-xs leading-relaxed">
{<br>
&nbsp;&nbsp;<span class="tok-key">"messages"</span>: [<br>
&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="tok-key">"message_id"</span>: <span class="tok-str">"msg_7a2f..."</span>,<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="tok-key">"from"</span>: <span class="tok-str">"+14155559999"</span>,<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="tok-key">"message"</span>: <span class="tok-str">"STOP"</span>,<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="tok-key">"received_at"</span>: <span class="tok-str">"2026-08-01T12:05:00Z"</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;}<br>
&nbsp;&nbsp;],<br>
&nbsp;&nbsp;<span class="tok-key">"total"</span>: <span class="tok-num">1</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"page"</span>: <span class="tok-num">1</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"pages"</span>: <span class="tok-num">1</span><br>
}
</div>
</section>
<!-- ============ OTP SEND ============ -->
<section id="otp-send" class="scroll-mt-24">
<div class="flex flex-wrap items-center gap-3">
<span class="rounded-md border px-2.5 py-1 font-mono-tech text-xs font-semibold badge-post">POST</span>
<code class="font-mono-tech text-lg text-white">/otp/send</code>
</div>
<h3 class="font-display mt-3 text-2xl font-bold text-white">Send an OTP</h3>
<p class="mt-2 text-slate-400">Generates a 6-digit code, sends it via SMS, and returns the expiry time. Max 3 codes per phone per hour.</p>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Request body</p>
<div class="mt-2 overflow-hidden rounded-xl glass">
<table class="min-w-full divide-y divide-white/10 text-sm">
<thead class="bg-white/5"><tr>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Parameter</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Type</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Required</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Default</th>
</tr></thead>
<tbody class="divide-y divide-white/5">
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">phone_number</td><td class="px-5 py-2 text-slate-400">string</td><td class="px-5 py-2 text-red-300">yes</td><td class="px-5 py-2 text-slate-500">—</td></tr>
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">purpose</td><td class="px-5 py-2 text-slate-400">string</td><td class="px-5 py-2 text-slate-500">no</td><td class="px-5 py-2 text-slate-400">authentication</td></tr>
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">expiry_minutes</td><td class="px-5 py-2 text-slate-400">integer</td><td class="px-5 py-2 text-slate-500">no</td><td class="px-5 py-2 text-slate-400">5</td></tr>
</tbody>
</table>
</div>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Response · 200 OK</p>
<div class="code-block mt-2 rounded-xl p-4 text-xs leading-relaxed">
{<br>
&nbsp;&nbsp;<span class="tok-key">"success"</span>: <span class="tok-bool">true</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"expires_at"</span>: <span class="tok-str">"2026-08-01T12:10:00Z"</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"message_id"</span>: <span class="tok-str">"msg_9b1c..."</span><br>
}
</div>
</section>
<!-- ============ OTP VERIFY ============ -->
<section id="otp-verify" class="scroll-mt-24">
<div class="flex flex-wrap items-center gap-3">
<span class="rounded-md border px-2.5 py-1 font-mono-tech text-xs font-semibold badge-post">POST</span>
<code class="font-mono-tech text-lg text-white">/otp/verify</code>
</div>
<h3 class="font-display mt-3 text-2xl font-bold text-white">Verify an OTP</h3>
<p class="mt-2 text-slate-400">Validates the 6-digit code. After 3 failed attempts the code is expired. On success the code is marked verified and cannot be reused.</p>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Request body</p>
<div class="mt-2 overflow-hidden rounded-xl glass">
<table class="min-w-full divide-y divide-white/10 text-sm">
<thead class="bg-white/5"><tr>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Parameter</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Type</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Required</th>
</tr></thead>
<tbody class="divide-y divide-white/5">
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">phone_number</td><td class="px-5 py-2 text-slate-400">string</td><td class="px-5 py-2 text-red-300">yes</td></tr>
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">code</td><td class="px-5 py-2 text-slate-400">string</td><td class="px-5 py-2 text-red-300">yes</td></tr>
</tbody>
</table>
</div>
<div class="mt-5 grid gap-4 sm:grid-cols-2">
<div>
<p class="font-mono-tech text-[10px] uppercase tracking-wider text-emerald-400">Success · 200</p>
<div class="code-block mt-2 rounded-xl p-4 text-xs leading-relaxed">
{<br>
&nbsp;&nbsp;<span class="tok-key">"success"</span>: <span class="tok-bool">true</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"verified"</span>: <span class="tok-bool">true</span><br>
}
</div>
</div>
<div>
<p class="font-mono-tech text-[10px] uppercase tracking-wider text-red-400">Failure · 200</p>
<div class="code-block mt-2 rounded-xl p-4 text-xs leading-relaxed">
{<br>
&nbsp;&nbsp;<span class="tok-key">"success"</span>: <span class="tok-bool">false</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"verified"</span>: <span class="tok-bool">false</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"error"</span>: <span class="tok-str">"Invalid or expired OTP"</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"attempts_remaining"</span>: <span class="tok-num">2</span><br>
}
</div>
</div>
</div>
</section>
<!-- ============ ADMIN STATS ============ -->
<section id="admin-stats" class="scroll-mt-24">
<div class="flex flex-wrap items-center gap-3">
<span class="rounded-md border px-2.5 py-1 font-mono-tech text-xs font-semibold badge-get">GET</span>
<code class="font-mono-tech text-lg text-white">/admin/stats</code>
</div>
<h3 class="font-display mt-3 text-2xl font-bold text-white">Admin Statistics</h3>
<p class="mt-2 text-slate-400">Aggregate system metrics: gateway counts, message throughput, pending/failed counts, and OTP performance for today.</p>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Response · 200 OK</p>
<div class="code-block mt-2 rounded-xl p-4 text-xs leading-relaxed">
{<br>
&nbsp;&nbsp;<span class="tok-key">"gateways"</span>: { <span class="tok-key">"total"</span>: <span class="tok-num">4</span>, <span class="tok-key">"active"</span>: <span class="tok-num">3</span>, <span class="tok-key">"online"</span>: <span class="tok-num">2</span>, <span class="tok-key">"offline"</span>: <span class="tok-num">2</span> },<br>
&nbsp;&nbsp;<span class="tok-key">"messages"</span>: {<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="tok-key">"total_sent"</span>: <span class="tok-num">10234</span>, <span class="tok-key">"total_received"</span>: <span class="tok-num">8821</span>,<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="tok-key">"sent_today"</span>: <span class="tok-num">142</span>, <span class="tok-key">"received_today"</span>: <span class="tok-num">98</span>,<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="tok-key">"total_today"</span>: <span class="tok-num">240</span>, <span class="tok-key">"pending"</span>: <span class="tok-num">3</span>, <span class="tok-key">"failed_today"</span>: <span class="tok-num">1</span><br>
&nbsp;&nbsp;},<br>
&nbsp;&nbsp;<span class="tok-key">"otp"</span>: { <span class="tok-key">"sent_today"</span>: <span class="tok-num">56</span>, <span class="tok-key">"verified_today"</span>: <span class="tok-num">49</span>, <span class="tok-key">"verification_rate"</span>: <span class="tok-num">87.5</span> },<br>
&nbsp;&nbsp;<span class="tok-key">"timestamp"</span>: <span class="tok-str">"2026-08-01T12:00:00Z"</span><br>
}
</div>
</section>
<!-- ============ ADMIN GATEWAYS ============ -->
<section id="admin-gateways" class="scroll-mt-24">
<div class="flex flex-wrap items-center gap-3">
<span class="rounded-md border px-2.5 py-1 font-mono-tech text-xs font-semibold badge-get">GET</span>
<code class="font-mono-tech text-lg text-white">/admin/gateways</code>
</div>
<h3 class="font-display mt-3 text-2xl font-bold text-white">List Gateways</h3>
<p class="mt-2 text-slate-400">Returns every registered gateway device with live status and counters.</p>
<div class="mt-5 flex flex-wrap items-center gap-3">
<span class="rounded-md border px-2.5 py-1 font-mono-tech text-xs font-semibold badge-post">POST</span>
<code class="font-mono-tech text-lg text-white">/admin/gateways/:id/toggle</code>
</div>
<p class="mt-2 text-slate-400">Activate or deactivate a gateway. Toggling off stops it from receiving outbound commands.</p>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Toggle response · 200 OK</p>
<div class="code-block mt-2 rounded-xl p-4 text-xs leading-relaxed">
{<br>
&nbsp;&nbsp;<span class="tok-key">"success"</span>: <span class="tok-bool">true</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"gateway"</span>: { <span class="tok-key">"id"</span>: <span class="tok-num">1</span>, <span class="tok-key">"device_id"</span>: <span class="tok-str">"pixel-001"</span>, <span class="tok-key">"active"</span>: <span class="tok-bool">false</span> }<br>
}
</div>
</section>
<!-- ===================== GATEWAY API DIVIDER ===================== -->
<div class="flex items-center gap-4">
<span class="h-px flex-1 bg-gradient-to-r from-transparent to-white/15"></span>
<span class="font-mono-tech text-[11px] uppercase tracking-[0.3em] text-sky-400/80">Gateway Device API</span>
<span class="h-px flex-1 bg-gradient-to-l from-transparent to-white/15"></span>
</div>
<!-- ============ GW REGISTER ============ -->
<section id="gw-register" class="scroll-mt-24">
<div class="flex flex-wrap items-center gap-3">
<span class="rounded-md border px-2.5 py-1 font-mono-tech text-xs font-semibold badge-post">POST</span>
<code class="font-mono-tech text-lg text-white">/gateway/register</code>
</div>
<h3 class="font-display mt-3 text-2xl font-bold text-white">Register a Gateway</h3>
<p class="mt-2 text-slate-400"><span class="rounded-md bg-emerald-500/10 px-2 py-0.5 font-mono-tech text-[10px] uppercase text-emerald-300">No auth</span> &nbsp; Registers an Android device and returns a fresh gateway API key. Returns <code class="font-mono-tech text-sky-300">409</code> if the device_id already exists.</p>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Request body</p>
<div class="mt-2 overflow-hidden rounded-xl glass">
<table class="min-w-full divide-y divide-white/10 text-sm">
<thead class="bg-white/5"><tr>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Parameter</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Type</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Required</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Description</th>
</tr></thead>
<tbody class="divide-y divide-white/5">
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">device_id</td><td class="px-5 py-2 text-slate-400">string</td><td class="px-5 py-2 text-red-300">yes</td><td class="px-5 py-2 text-slate-400">Unique device identifier</td></tr>
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">name</td><td class="px-5 py-2 text-slate-400">string</td><td class="px-5 py-2 text-slate-500">no</td><td class="px-5 py-2 text-slate-400">Friendly name (defaults to device id prefix)</td></tr>
</tbody>
</table>
</div>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Response · 201 Created</p>
<div class="code-block mt-2 rounded-xl p-4 text-xs leading-relaxed">
{<br>
&nbsp;&nbsp;<span class="tok-key">"success"</span>: <span class="tok-bool">true</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"api_key"</span>: <span class="tok-str">"gw_live_a1b2c3..."</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"device_id"</span>: <span class="tok-str">"pixel-001"</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"websocket_url"</span>: <span class="tok-str">"wss://your-host/cable"</span><br>
}
</div>
<p class="mt-3 rounded-lg border border-amber-400/20 bg-amber-500/5 px-4 py-2 text-xs text-amber-200/80"><i class="fas fa-triangle-exclamation"></i> Save <code class="font-mono-tech">api_key</code> immediately — it is never shown again.</p>
</section>
<!-- ============ GW HEARTBEAT ============ -->
<section id="gw-heartbeat" class="scroll-mt-24">
<div class="flex flex-wrap items-center gap-3">
<span class="rounded-md border px-2.5 py-1 font-mono-tech text-xs font-semibold badge-post">POST</span>
<code class="font-mono-tech text-lg text-white">/gateway/heartbeat</code>
</div>
<h3 class="font-display mt-3 text-2xl font-bold text-white">Gateway Heartbeat</h3>
<p class="mt-2 text-slate-400">Refreshes the gateway's online status. A gateway is marked offline if no heartbeat arrives within 2 minutes (handled by <code class="font-mono-tech text-sky-300">CheckGatewayHealthJob</code>).</p>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Optional body</p>
<div class="mt-2 overflow-hidden rounded-xl glass">
<table class="min-w-full divide-y divide-white/10 text-sm">
<thead class="bg-white/5"><tr>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Parameter</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Type</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Description</th>
</tr></thead>
<tbody class="divide-y divide-white/5">
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">battery_level</td><td class="px-5 py-2 text-slate-400">integer</td><td class="px-5 py-2 text-slate-400">0100</td></tr>
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">signal_strength</td><td class="px-5 py-2 text-slate-400">integer</td><td class="px-5 py-2 text-slate-400">dBm</td></tr>
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">messages_in_queue</td><td class="px-5 py-2 text-slate-400">integer</td><td class="px-5 py-2 text-slate-400">Pending outbound count on device</td></tr>
</tbody>
</table>
</div>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Response · 200 OK</p>
<div class="code-block mt-2 rounded-xl p-4 text-xs leading-relaxed">
{ <span class="tok-key">"success"</span>: <span class="tok-bool">true</span>, <span class="tok-key">"pending_messages"</span>: <span class="tok-num">3</span> }
</div>
</section>
<!-- ============ GW SMS RECEIVED ============ -->
<section id="gw-sms-received" class="scroll-mt-24">
<div class="flex flex-wrap items-center gap-3">
<span class="rounded-md border px-2.5 py-1 font-mono-tech text-xs font-semibold badge-post">POST</span>
<code class="font-mono-tech text-lg text-white">/gateway/sms/received</code>
</div>
<h3 class="font-display mt-3 text-2xl font-bold text-white">Report Inbound SMS</h3>
<p class="mt-2 text-slate-400">Called by the gateway when a new SMS arrives on the device. Creates an inbound record and triggers <code class="font-mono-tech text-sky-300">ProcessInboundSmsJob</code> (which fires webhooks).</p>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Request body</p>
<div class="mt-2 overflow-hidden rounded-xl glass">
<table class="min-w-full divide-y divide-white/10 text-sm">
<thead class="bg-white/5"><tr>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Parameter</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Type</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Required</th>
</tr></thead>
<tbody class="divide-y divide-white/5">
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">sender</td><td class="px-5 py-2 text-slate-400">string</td><td class="px-5 py-2 text-red-300">yes</td></tr>
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">message</td><td class="px-5 py-2 text-slate-400">string</td><td class="px-5 py-2 text-red-300">yes</td></tr>
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">timestamp</td><td class="px-5 py-2 text-slate-400">datetime</td><td class="px-5 py-2 text-slate-500">no</td></tr>
</tbody>
</table>
</div>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Response · 200 OK</p>
<div class="code-block mt-2 rounded-xl p-4 text-xs leading-relaxed">
{ <span class="tok-key">"success"</span>: <span class="tok-bool">true</span>, <span class="tok-key">"message_id"</span>: <span class="tok-str">"msg_7a2f..."</span> }
</div>
</section>
<!-- ============ GW SMS STATUS ============ -->
<section id="gw-sms-status" class="scroll-mt-24">
<div class="flex flex-wrap items-center gap-3">
<span class="rounded-md border px-2.5 py-1 font-mono-tech text-xs font-semibold badge-post">POST</span>
<code class="font-mono-tech text-lg text-white">/gateway/sms/status</code>
</div>
<h3 class="font-display mt-3 text-2xl font-bold text-white">Report Delivery Status</h3>
<p class="mt-2 text-slate-400">Updates an outbound message's status. On <code class="font-mono-tech text-sky-300">failed</code>, <code class="font-mono-tech text-sky-300">RetryFailedSmsJob</code> is enqueued if retries remain (&lt; 3).</p>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Request body</p>
<div class="mt-2 overflow-hidden rounded-xl glass">
<table class="min-w-full divide-y divide-white/10 text-sm">
<thead class="bg-white/5"><tr>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Parameter</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Type</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Required</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Values</th>
</tr></thead>
<tbody class="divide-y divide-white/5">
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">message_id</td><td class="px-5 py-2 text-slate-400">string</td><td class="px-5 py-2 text-red-300">yes</td><td class="px-5 py-2 text-slate-500">—</td></tr>
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">status</td><td class="px-5 py-2 text-slate-400">string</td><td class="px-5 py-2 text-red-300">yes</td><td class="px-5 py-2 text-slate-400">sent · delivered · failed</td></tr>
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">error_message</td><td class="px-5 py-2 text-slate-400">string</td><td class="px-5 py-2 text-slate-500">no</td><td class="px-5 py-2 text-slate-400">Reason for failure</td></tr>
</tbody>
</table>
</div>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Response · 200 OK</p>
<div class="code-block mt-2 rounded-xl p-4 text-xs leading-relaxed">
{ <span class="tok-key">"success"</span>: <span class="tok-bool">true</span> }
</div>
</section>
<!-- ===================== REALTIME DIVIDER ===================== -->
<div class="flex items-center gap-4">
<span class="h-px flex-1 bg-gradient-to-r from-transparent to-white/15"></span>
<span class="font-mono-tech text-[11px] uppercase tracking-[0.3em] text-sky-400/80">Realtime</span>
<span class="h-px flex-1 bg-gradient-to-l from-transparent to-white/15"></span>
</div>
<!-- ============ WEBSOCKET ============ -->
<section id="websocket" class="scroll-mt-24">
<div class="flex flex-wrap items-center gap-3">
<span class="rounded-md border px-2.5 py-1 font-mono-tech text-xs font-semibold badge-ws">WS</span>
<code class="font-mono-tech text-lg text-white">/cable</code>
</div>
<h3 class="font-display mt-3 text-2xl font-bold text-white">WebSocket Channel</h3>
<p class="mt-2 text-slate-400">Gateways maintain a persistent Action Cable connection for real-time command dispatch and status reporting. Connections authenticate with the gateway API key as a query param.</p>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Connect</p>
<div class="code-block mt-2 rounded-xl p-4 text-xs leading-relaxed">
<span class="tok-key">wscat</span> -c <span class="tok-str">"wss://your-host/cable?api_key=gw_live_..."</span>
</div>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Server → Gateway commands</p>
<div class="code-block mt-2 rounded-xl p-4 text-xs leading-relaxed">
{<br>
&nbsp;&nbsp;<span class="tok-key">"action"</span>: <span class="tok-str">"send_sms"</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"message_id"</span>: <span class="tok-str">"msg_3f8a..."</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"recipient"</span>: <span class="tok-str">"+14155551234"</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"message"</span>: <span class="tok-str">"Hello!"</span><br>
}
</div>
<p class="mt-5 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Gateway → Server events</p>
<div class="mt-2 overflow-hidden rounded-xl glass">
<table class="min-w-full divide-y divide-white/10 text-sm">
<thead class="bg-white/5"><tr>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">action</th>
<th class="px-5 py-2 text-left font-mono-tech text-[10px] uppercase tracking-wider text-slate-400">Payload</th>
</tr></thead>
<tbody class="divide-y divide-white/5">
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">heartbeat</td><td class="px-5 py-2 text-slate-400">battery_level, signal_strength, messages_in_queue</td></tr>
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">delivery_report</td><td class="px-5 py-2 text-slate-400">message_id, status, error_message</td></tr>
<tr><td class="px-5 py-2 font-mono-tech text-sky-300">message_received</td><td class="px-5 py-2 text-slate-400">sender, message, timestamp</td></tr>
</tbody>
</table>
</div>
</section>
<!-- ============ WEBHOOKS ============ -->
<section id="webhooks" class="scroll-mt-24">
<div class="flex flex-wrap items-center gap-3">
<span class="rounded-md border px-2.5 py-1 font-mono-tech text-xs font-semibold badge-post">WEBHOOK</span>
<code class="font-mono-tech text-lg text-white">Outbound HTTP POST</code>
</div>
<h3 class="font-display mt-3 text-2xl font-bold text-white">Webhooks</h3>
<p class="mt-2 text-slate-400">When configured, the API delivers signed event payloads to your URL via <code class="font-mono-tech text-sky-300">TriggerWebhookJob</code>. If a secret key is set, each request is signed with HMAC-SHA256.</p>
<div class="mt-6 grid gap-4 sm:grid-cols-3">
<% events = [
{ event: "sms_received", icon: "fa-arrow-down", color: "text-emerald-400", desc: "Inbound SMS arrived" },
{ event: "sms_sent", icon: "fa-paper-plane", color: "text-sky-400", desc: "Message dispatched to gateway" },
{ event: "sms_failed", icon: "fa-circle-xmark", color: "text-red-400", desc: "Delivery failed" }
] %>
<% events.each do |e| %>
<div class="rounded-xl glass p-5">
<i class="fas <%= e[:icon] %> <%= e[:color] %>"></i>
<code class="mt-2 block font-mono-tech text-sm text-white"><%= e[:event] %></code>
<p class="mt-1 text-xs text-slate-400"><%= e[:desc] %></p>
</div>
<% end %>
</div>
<p class="mt-6 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Example payload (sms_received)</p>
<div class="code-block mt-2 rounded-xl p-4 text-xs leading-relaxed">
{<br>
&nbsp;&nbsp;<span class="tok-key">"event"</span>: <span class="tok-str">"sms_received"</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"message_id"</span>: <span class="tok-str">"msg_7a2f..."</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"from"</span>: <span class="tok-str">"+14155559999"</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"message"</span>: <span class="tok-str">"Confirm"</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"received_at"</span>: <span class="tok-str">"2026-08-01T12:05:00Z"</span><br>
}
</div>
<p class="mt-6 font-mono-tech text-[10px] uppercase tracking-wider text-slate-500">Verifying the signature</p>
<div class="code-block mt-2 rounded-xl p-4 text-xs leading-relaxed">
<span class="tok-com"># Ruby</span><br>
signature = OpenSSL::HMAC.hexdigest(<span class="tok-str">"SHA256"</span>, secret_key, payload.to_json)<br>
<span class="tok-com"># Compare with the X-Webhook-Signature header</span>
</div>
</section>
<!-- Back to top -->
<div class="pt-10 text-center">
<a href="#top" class="btn-ghost inline-flex items-center gap-2 rounded-xl px-5 py-2.5 text-sm font-semibold text-slate-200">
<i class="fas fa-arrow-up"></i> Back to top
</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,242 @@
<!-- ===================== HERO ===================== -->
<section class="relative px-5 pt-20 pb-24 sm:px-8">
<div class="mx-auto max-w-5xl text-center">
<!-- Badge -->
<div class="fade-up inline-flex items-center gap-2 rounded-full glass px-4 py-1.5 text-xs">
<span class="relative flex h-2 w-2">
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-sky-400 opacity-75"></span>
<span class="relative inline-flex h-2 w-2 rounded-full bg-sky-400"></span>
</span>
<span class="font-mono-tech uppercase tracking-[0.25em] text-slate-300">SMS Gateway · API v1</span>
</div>
<!-- Beacon logo -->
<div class="fade-up d-1 relative mx-auto mt-10 h-24 w-24 flex items-center justify-center">
<span class="beacon-ring absolute inset-0 rounded-full border border-sky-400/40"></span>
<span class="beacon-ring delay-1 absolute inset-0 rounded-full border border-sky-400/30"></span>
<span class="beacon-ring delay-2 absolute inset-0 rounded-full border border-sky-400/20"></span>
<div class="relative flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-sky-400 to-sky-600 shadow-xl shadow-sky-500/40">
<i class="fas fa-sms text-3xl text-slate-950"></i>
</div>
</div>
<h1 class="fade-up d-2 font-display mt-8 text-5xl font-extrabold tracking-tight text-white sm:text-7xl">
Send SMS through your <span class="gradient-text">Android devices</span>
</h1>
<p class="fade-up d-3 mx-auto mt-6 max-w-2xl text-lg text-slate-400 sm:text-xl">
A production-grade REST &amp; WebSocket API that turns any Android phone into a
programmable SMS gateway. Send messages, generate OTPs, receive inbound texts,
and orchestrate fleets of devices in real time.
</p>
<div class="fade-up d-4 mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row">
<%= link_to docs_path, class: "btn-accent inline-flex items-center gap-2 rounded-xl px-7 py-3.5 text-base font-semibold text-slate-950" do %>
<i class="fas fa-book-open"></i> Explore the API
<% end %>
<%= link_to admin_login_path, class: "btn-ghost inline-flex items-center gap-2 rounded-xl px-7 py-3.5 text-base font-semibold text-slate-200" do %>
<i class="fas fa-gauge-high"></i> Admin Console
<% end %>
</div>
<!-- Base URL chip -->
<div class="fade-up d-5 mx-auto mt-12 inline-flex items-center gap-3 rounded-xl glass px-5 py-3">
<span class="font-mono-tech text-[11px] uppercase tracking-wider text-slate-500">Base URL</span>
<code class="font-mono-tech text-sm text-sky-300">https://&lt;your-host&gt;/api/v1</code>
<button data-copy="https://your-host/api/v1" class="copy-btn text-slate-500 hover:text-sky-400" aria-label="Copy base URL">
<i class="far fa-copy"></i>
</button>
</div>
</div>
</section>
<!-- ===================== STAT STRIP ===================== -->
<section class="relative px-5 sm:px-8">
<div class="mx-auto max-w-5xl">
<div class="grid grid-cols-2 gap-4 rounded-2xl glass px-6 py-6 sm:grid-cols-4">
<% stats = [
{ icon: "fa-bolt", value: "< 200ms", label: "API latency" },
{ icon: "fa-infinity", value: "Unlimited", label: "Gateways" },
{ icon: "fa-shield-halved", value: "SHA-256", label: "Key hashing" },
{ icon: "fa-rotate", value: "3× retry", label: "Auto backoff" }
] %>
<% stats.each do |s| %>
<div class="flex flex-col items-center text-center">
<i class="fas <%= s[:icon] %> text-sky-400"></i>
<span class="mt-2 font-display text-xl font-bold text-white"><%= s[:value] %></span>
<span class="font-mono-tech text-[10px] uppercase tracking-[0.2em] text-slate-500"><%= s[:label] %></span>
</div>
<% end %>
</div>
</div>
</section>
<!-- ===================== FEATURES ===================== -->
<section class="relative px-5 py-24 sm:px-8">
<div class="mx-auto max-w-6xl">
<div class="mx-auto max-w-2xl text-center">
<p class="font-mono-tech text-[11px] uppercase tracking-[0.3em] text-sky-400/80">Capabilities</p>
<h2 class="font-display mt-3 text-4xl font-bold text-white sm:text-5xl">Everything you need to route SMS at scale</h2>
<p class="mt-4 text-slate-400">From a single device to a distributed fleet — message queuing, delivery tracking, and OTP workflows are built in.</p>
</div>
<div class="mt-14 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<% features = [
{ icon: "fa-paper-plane", color: "from-sky-400 to-sky-600", title: "Send SMS", desc: "Queue outbound messages and let the gateway fleet deliver them. Automatic retries with exponential backoff." },
{ icon: "fa-arrow-down", color: "from-emerald-400 to-emerald-600", title: "Receive SMS", desc: "Inbound messages land in your inbox instantly and fire webhooks so your app reacts in real time." },
{ icon: "fa-shield-halved", color: "from-violet-400 to-violet-600", title: "OTP Generation", desc: "Six-digit codes with 5-minute expiry, 3-attempt limits, and 3-per-hour rate caps per phone number." },
{ icon: "fa-plug", color: "from-amber-400 to-orange-600", title: "Webhooks", desc: "HMAC-signed event payloads for sms_received, sms_sent and sms_failed with configurable retries." },
{ icon: "fa-tower-broadcast", color: "from-pink-400 to-rose-600", title: "Realtime WebSocket", desc: "Bidirectional Action Cable channel keeps every gateway in sync — heartbeats, delivery reports, and commands." },
{ icon: "fa-mobile-screen", color: "from-blue-400 to-indigo-600", title: "Gateway Fleet", desc: "Register unlimited Android devices. Priority-based routing, health checks, and live status monitoring." }
] %>
<% features.each_with_index do |f, i| %>
<div class="fade-up d-<%= (i % 3) + 1 %> group rounded-2xl glass p-6 transition-all duration-300 hover:-translate-y-1 hover:border-sky-400/30">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br <%= f[:color] %> shadow-lg">
<i class="fas <%= f[:icon] %> text-lg text-slate-950"></i>
</div>
<h3 class="font-display mt-5 text-xl font-bold text-white"><%= f[:title] %></h3>
<p class="mt-2 text-sm leading-relaxed text-slate-400"><%= f[:desc] %></p>
</div>
<% end %>
</div>
</div>
</section>
<!-- ===================== ARCHITECTURE ===================== -->
<section class="relative px-5 py-24 sm:px-8">
<div class="mx-auto max-w-6xl">
<div class="mx-auto max-w-2xl text-center">
<p class="font-mono-tech text-[11px] uppercase tracking-[0.3em] text-sky-400/80">Architecture</p>
<h2 class="font-display mt-3 text-4xl font-bold text-white sm:text-5xl">How messages flow</h2>
<p class="mt-4 text-slate-400">A clean, asynchronous pipeline keeps the API fast and delivery resilient.</p>
</div>
<div class="mt-14 grid items-stretch gap-4 lg:grid-cols-5">
<% steps = [
{ n: "01", icon: "fa-code", title: "Client calls API", desc: "POST /api/v1/sms/send with a Bearer API key. Phone numbers are validated with Phonelib." },
{ n: "02", icon: "fa-database", title: "Message queued", desc: "SmsMessage is persisted and SendSmsJob is enqueued via Sidekiq. The API returns instantly with a message_id." },
{ n: "03", icon: "fa-route", title: "Gateway selected", desc: "SendSmsJob picks the highest-priority online, active gateway and broadcasts over Action Cable." },
{ n: "04", icon: "fa-mobile-screen-button", title: "Android delivers", desc: "The gateway device sends the SMS via its native radio and reports back a delivery status." },
{ n: "05", icon: "fa-circle-check", title: "Status & webhooks", desc: "Delivery/failed status is recorded; webhooks fire for subscribed events. Failed messages auto-retry up to 3×." }
] %>
<% steps.each do |s| %>
<div class="relative rounded-2xl glass p-6">
<span class="font-mono-tech text-xs text-sky-400/70"><%= s[:n] %></span>
<i class="fas <%= s[:icon] %> mt-3 text-2xl text-sky-400"></i>
<h3 class="font-display mt-3 text-base font-bold text-white"><%= s[:title] %></h3>
<p class="mt-2 text-xs leading-relaxed text-slate-400"><%= s[:desc] %></p>
</div>
<% end %>
</div>
</div>
</section>
<!-- ===================== QUICK START ===================== -->
<section class="relative px-5 py-24 sm:px-8">
<div class="mx-auto max-w-5xl">
<div class="text-center">
<p class="font-mono-tech text-[11px] uppercase tracking-[0.3em] text-sky-400/80">Quick Start</p>
<h2 class="font-display mt-3 text-4xl font-bold text-white sm:text-5xl">Send your first message</h2>
</div>
<div class="mt-12 grid gap-6 lg:grid-cols-2">
<!-- Step 1 -->
<div class="rounded-2xl glass p-6">
<div class="flex items-center gap-3">
<span class="flex h-7 w-7 items-center justify-center rounded-full bg-sky-500 font-mono-tech text-xs font-bold text-slate-950">1</span>
<h3 class="font-display text-lg font-bold text-white">Register a gateway device</h3>
</div>
<p class="mt-3 text-sm text-slate-400">No auth required — receive a gateway API key back.</p>
<div class="code-block mt-4 rounded-xl p-4 text-xs leading-relaxed">
<span class="tok-com"># Register your Android device</span><br>
<span class="tok-key">curl</span> -X POST https://your-host/api/v1/gateway/register \<br>
&nbsp;&nbsp;-H <span class="tok-str">"Content-Type: application/json"</span> \<br>
&nbsp;&nbsp;-d <span class="tok-str">'{"device_id":"pixel-001","name":"Office Phone"}'</span>
</div>
<div class="mt-3 flex items-center gap-2">
<button data-copy='curl -X POST https://your-host/api/v1/gateway/register -H "Content-Type: application/json" -d "{\"device_id\":\"pixel-001\",\"name\":\"Office Phone\"}"' class="copy-btn text-xs text-slate-500 hover:text-sky-400"><i class="far fa-copy"></i> Copy</button>
</div>
</div>
<!-- Step 2 -->
<div class="rounded-2xl glass p-6">
<div class="flex items-center gap-3">
<span class="flex h-7 w-7 items-center justify-center rounded-full bg-sky-500 font-mono-tech text-xs font-bold text-slate-950">2</span>
<h3 class="font-display text-lg font-bold text-white">Connect via WebSocket</h3>
</div>
<p class="mt-3 text-sm text-slate-400">Keep the device online and ready to receive commands.</p>
<div class="code-block mt-4 rounded-xl p-4 text-xs leading-relaxed">
<span class="tok-com"># wscat</span><br>
<span class="tok-key">wscat</span> -c <span class="tok-str">"ws://your-host:3000/cable?api_key=gw_live_..."</span>
</div>
</div>
<!-- Step 3 -->
<div class="rounded-2xl glass p-6 lg:col-span-2">
<div class="flex items-center gap-3">
<span class="flex h-7 w-7 items-center justify-center rounded-full bg-sky-500 font-mono-tech text-xs font-bold text-slate-950">3</span>
<h3 class="font-display text-lg font-bold text-white">Send an SMS</h3>
</div>
<p class="mt-3 text-sm text-slate-400">Use a client API key (api_live_…) in the Authorization header.</p>
<div class="code-block mt-4 rounded-xl p-4 text-xs leading-relaxed">
<span class="tok-key">curl</span> -X POST https://your-host/api/v1/sms/send \<br>
&nbsp;&nbsp;-H <span class="tok-str">"Authorization: Bearer api_live_..."</span> \<br>
&nbsp;&nbsp;-H <span class="tok-str">"Content-Type: application/json"</span> \<br>
&nbsp;&nbsp;-d <span class="tok-str">'{"to":"+14155551234","message":"Hello from MySMSAPio!"}'</span>
</div>
<div class="mt-3 flex items-center gap-2">
<button data-copy='curl -X POST https://your-host/api/v1/sms/send -H "Authorization: Bearer api_live_..." -H "Content-Type: application/json" -d "{\"to\":\"+14155551234\",\"message\":\"Hello from MySMSAPio!\"}"' class="copy-btn text-xs text-slate-500 hover:text-sky-400"><i class="far fa-copy"></i> Copy</button>
</div>
<div class="code-block mt-4 rounded-xl p-4 text-xs leading-relaxed">
<span class="tok-com"># → 202 Accepted</span><br>
{<br>
&nbsp;&nbsp;<span class="tok-key">"success"</span>: <span class="tok-bool">true</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"message_id"</span>: <span class="tok-str">"msg_3f8a9c1b2e7d4f60"</span>,<br>
&nbsp;&nbsp;<span class="tok-key">"status"</span>: <span class="tok-str">"queued"</span><br>
}
</div>
</div>
</div>
<div class="mt-10 text-center">
<%= link_to docs_path, class: "btn-ghost inline-flex items-center gap-2 rounded-xl px-6 py-3 text-sm font-semibold text-slate-200" do %>
<i class="fas fa-arrow-right-long"></i> Read the full API reference
<% end %>
</div>
</div>
</section>
<!-- ===================== TECH STACK ===================== -->
<section class="relative px-5 py-24 sm:px-8">
<div class="mx-auto max-w-5xl">
<div class="text-center">
<p class="font-mono-tech text-[11px] uppercase tracking-[0.3em] text-sky-400/80">Built on a modern stack</p>
<h2 class="font-display mt-3 text-4xl font-bold text-white sm:text-5xl">Production-ready by default</h2>
</div>
<div class="mt-12 flex flex-wrap items-center justify-center gap-3">
<% stack = ["Rails 8.0", "Ruby 3.4", "PostgreSQL", "Redis 7", "Sidekiq 7", "Action Cable", "Puma + Thruster", "Docker / Kamal", "Tailwind CSS", "Phonelib", "HTTParty", "ROTP"] %>
<% stack.each do |tech| %>
<span class="rounded-full glass px-5 py-2 font-mono-tech text-xs text-slate-300"><%= tech %></span>
<% end %>
</div>
</div>
</section>
<!-- ===================== CTA ===================== -->
<section class="relative px-5 py-24 sm:px-8">
<div class="mx-auto max-w-4xl">
<div class="relative overflow-hidden rounded-3xl glass-strong px-8 py-16 text-center sm:px-16">
<div class="pointer-events-none absolute -top-24 left-1/2 h-64 w-64 -translate-x-1/2 rounded-full bg-sky-500/20 blur-3xl"></div>
<h2 class="font-display relative text-4xl font-bold text-white sm:text-5xl">Ready to send your first SMS?</h2>
<p class="relative mx-auto mt-4 max-w-xl text-slate-400">Dive into the complete API reference or jump straight into the admin console to manage your gateways.</p>
<div class="relative mt-8 flex flex-col items-center justify-center gap-4 sm:flex-row">
<%= link_to docs_path, class: "btn-accent inline-flex items-center gap-2 rounded-xl px-7 py-3.5 text-base font-semibold text-slate-950" do %>
<i class="fas fa-book"></i> API Documentation
<% end %>
<%= link_to admin_login_path, class: "btn-ghost inline-flex items-center gap-2 rounded-xl px-7 py-3.5 text-base font-semibold text-slate-200" do %>
<i class="fas fa-gauge-high"></i> Open Admin Console
<% end %>
</div>
</div>
</div>
</section>

View File

@@ -13,10 +13,8 @@ Rails.application.routes.draw do
post "register", to: "registrations#create"
post "heartbeat", to: "heartbeats#create"
namespace :sms do
post "received", to: "sms#received"
post "status", to: "sms#status"
end
post "sms/received", to: "sms#received"
post "sms/status", to: "sms#status"
end
# Client application endpoints
@@ -25,10 +23,8 @@ Rails.application.routes.draw do
get "sms/received", to: "sms#received"
# OTP endpoints
namespace :otp do
post "send", to: "otp#send_otp"
post "verify", to: "otp#verify"
end
post "otp/send", to: "otp#send_otp"
post "otp/verify", to: "otp#verify"
# Admin endpoints
namespace :admin do
@@ -72,6 +68,7 @@ Rails.application.routes.draw do
end
end
# Root route
root to: proc { [200, {}, ["SMS Gateway API v1.0"]] }
# Public pages
get "docs", to: "pages#docs"
root to: "pages#home"
end