Debugging webhooks locally can be a painful experience. Whether you are integrating Stripe payments, GitHub events, or custom API callbacks, testing them on localhost usually requires tunneling tools like ngrok. While these tools are fantastic, sometimes you want a persistent, self-hosted solution that lives on your own server—a place where you can inspect, replay, and generate code from incoming HTTP requests.
In this post, I’ll walk through how I built a lightweight, single-file Webhook Inspector using PHP for the backend and Vanilla JavaScript for the frontend.
The Architecture
The goal was to keep dependencies zero and the code portable. I designed the tool to operate in two modes within a single index.php file:
- The Collector (API Mode): When the script receives a request with a specific
uuidparameter, it captures the headers, body, and query parameters, saves them to a local JSON file, and responds with200 OK. - The Viewer (UI Mode): When the script is accessed via a browser (without a webhook UUID), it serves a modern HTML dashboard. This dashboard polls the JSON file to render incoming requests in real-time.
graph TB
subgraph Client ["Client Layer (Browser)"]
Browser["User Browser"]
JS["JavaScript Engine<br/>(Logic, Polling, Code Gen)"]
LS["LocalStorage<br/>(Session UUID)"]
end
subgraph Server ["Server Layer (index.php - Single File)"]
Router["Request Router<br/>(Dispatch Logic)"]
WH["Webhook Listener<br/>(Capture & Parse)"]
API["JSON API<br/>(Create Session / Read Data)"]
UI["HTML Renderer<br/>(Dashboard UI)"]
end
subgraph Storage ["Data Layer (Disk)"]
DB["webhook_db.json<br/>(Flat-File NoSQL Store)"]
end
subgraph External ["External Sources"]
ExtAPI["3rd Party APIs<br/>(Stripe, GitHub, Custom)"]
end
%% Client Interactions
Browser <--> JS
JS <--> LS
Browser -->|"GET / (View)"| Router
Router -->|"HTML Response"| UI
UI -->|"DOM"| Browser
JS -->|"AJAX: ?hook_api=create"| Router
JS -->|"AJAX: ?hook_api=data (Poll)"| Router
%% External Interactions
ExtAPI -->|"POST /?uuid=xyz (Webhook)"| Router
%% Internal Server Routing
Router -->|"Dispatch"| WH
Router -->|"Dispatch"| API
%% Data Interactions
WH -->|"Write Payload"| DB
API -->|"Read Payload"| DB
%% API Responses
API -->|"JSON Data"| Router
WH -->|"200 OK"| Router
Router -->|"JSON"| Browser
Router -->|"Ack"| ExtAPI
%% Styling
style Router fill:#e1f5fe,stroke:#01579b,stroke-width:3px
style DB fill:#fff9c4,stroke:#fbc02d,stroke-width:2px
style Server fill:#f3f4f6,stroke:#374151,stroke-dasharray: 5 5- Client Layer:
- Browser: The standard web interface.
- JavaScript Engine: Handles the “New Webhook” button generation, sets up the 2-second polling interval, and renders the request list/inspector tabs.
- LocalStorage: Persists the UUID so the user doesn’t lose their session on page refresh.
- Server Layer (index.php):
- Request Router: The entry point. It inspects
$_GETparameters to decide if the request should go to the Webhook Listener, the JSON API, or the HTML Renderer. - Webhook Listener: Parses
php://input, extracts headers, and prepares the data array. - JSON API: Handles two specific tasks: generating a new UUID (Create) and serving the list of requests for a specific UUID (Read).
- HTML Renderer: Serves the dashboard HTML when accessed normally without API parameters.
- Request Router: The entry point. It inspects
- Data Layer:
- webhook_db.json: Acts as the database. It is a flat JSON file where keys are UUIDs and values are arrays of request objects. No MySQL or other RDBMS is required.
The Challenge: Multi-Session Support
Initially, the tool dumped all requests into a single array. This was fine for one user, but in a multi-user environment, everyone would see everyone else’s webhooks. To solve this without setting up a MySQL database, I implemented a UUID-based session system.
When a user clicks “New Webhook,” the JavaScript generates a cryptographically secure random UUID:
const response = await fetch('?hook_api=create');
On the backend, PHP creates a new entry in a webhook_db.json file using this UUID as the key:
$data[$newUuid] = []; // Initialize empty array for this session
file_put_contents($dbFile, json_encode($data, JSON_PRETTY_PRINT));
This structure allows the JSON file to act as a NoSQL document store, where every UUID key holds its own isolated array of requests.
The Backend: Capturing the Request
PHP is uniquely suited for this because of its superglobal variables ($_SERVER, $_GET). The capture logic is triggered by checking for the presence of a UUID in the query string:
php
if (isset($_GET['uuid']) && !isset($_GET['view'])) {
$rawBody = file_get_contents('php://input');
$parsedBody = json_decode($rawBody, true);
// Capture details
$newRequest = [
'id' => uniqid(),
'timestamp' => time(),
'method' => $_SERVER['REQUEST_METHOD'],
'headers' => getallheaders(),
'body' => $parsedBody
];
// Prepend to the array (newest first)
array_unshift($data[$_GET['uuid']], $newRequest);
// Save logic...
}
We use file_get_contents('php://input') to capture the raw body, which allows us to handle JSON payloads that $_POST might miss.
The Frontend: Real-Time Polling
Since we aren’t using WebSockets (to keep the “single file” constraint simple), the dashboard uses short-interval polling. Every 2 seconds, the frontend sends a request to the backend asking specifically for data related to its UUID:
javascript
asyncfunctionfetchRequests() {
// Only fetch data for the current user's UUID
constjsonRes=awaitfetch(`?hook_api=data&uuid=${currentUuid}`);
constdata=awaitjsonRes.json();
// Update DOM if data changed
if (JSON.stringify(data) !==JSON.stringify(allRequests)) {
allRequests=data;
renderList();
}
}
While polling isn’t as efficient as WebSockets for high-frequency events, for webhook debugging (which is usually event-driven and low-volume), it is more than sufficient and significantly easier to deploy on standard shared hosting.
sequenceDiagram
autonumber
actor User as User Browser
participant App as index.php (Server Logic)
participant DB as webhook_db.json (File Storage)
participant API as External API (e.g. Stripe/GitHub)
Note over User, API: Phase 1: Initialization
User->>App: GET / (Request Dashboard UI)
App-->>User: Return HTML + CSS + JS
User->>App: GET ?hook_api=create
App->>App: Generate Random UUID
App->>DB: Update JSON (Create new UUID Key)
DB-->>App: Save Confirmation
App-->>User: JSON { uuid: "abc-123..." }
User->>User: Store UUID in LocalStorage
Note over User, API: Phase 2: Real-Time Polling (Background)
loop Every 2 Seconds
User->>App: GET ?hook_api=data&uuid=abc-123...
App->>DB: Read JSON for key "abc-123..."
DB-->>App: Return Request Array
App-->>User: JSON Request List
User->>User: Update DOM (Sidebar/Inspector)
end
Note over User, API: Phase 3: Incoming Webhook Capture
API->>App: POST /?uuid=abc-123... (Payload)
Note right of API: Third-party service sending event
App->>App: Parse Headers & php://input
App->>DB: Append Request to key "abc-123..."
DB-->>App: Write Success
App-->>API: 200 OK (Acknowledgement)
Note over User, API: Phase 4: Visualization
User->>App: GET ?hook_api=data&uuid=abc-123...
App-->>User: JSON (Includes new Webhook)
User->>User: Display New RequestDeveloper Experience: Code Generation
One of the most useful features of this tool is the ability to turn an incoming request into a code snippet instantly. If I receive a complex webhook from a third-party API, I can select it and generate a cURL command (or JavaScript/PHP/Python) to replay it in my terminal.
The JavaScript logic constructs these strings using template literals:
javascript
if (lang==='curl') {
code=`curl -X ${req.method} "${webhookUrl}" \\\n`;
// Iterate headers
for (const [k, v] ofObject.entries(req.headers)) {
code+=` -H "${k}: ${v}" \\\n`;
}
// Add body if present
if (['POST', 'PUT', 'PATCH'].includes(req.method) &&bodyStr) {
code+=` -d '${safeBody}'`;
}
}
This effectively turns the inspector into a learning tool for API integrations.
Security Considerations
While this tool is powerful, hosting it publicly requires caution. Because it accepts any HTTP method and writes to a file, it exposes a few attack vectors:
- Denial of Service (DoS): An attacker could spam the endpoint, filling the disk with JSON data or spiking I/O usage.
- Information Disclosure: Without authentication, anyone with the URL can view the webhooks.
For production use, I recommend:
- Adding IP Whitelisting (e.g., only allow requests from known webhook providers like GitHub or Stripe).
- Password protecting the directory with
.htaccess. - Running it inside a Docker container or VM rather than on a shared host.
Conclusion
This Webhook Inspector proves that you don’t need a heavy framework or a complex database to build useful developer tools. By leveraging PHP’s built-in superglobals and the modern Fetch API, we created a portable, self-contained debugging environment that lives entirely in a single file.
Full Code
<?php
/**
* START OF BACKEND LOGIC
* We use ob_start() to catch any accidental whitespace before the JSON output.
*/
ob_start();
$dbFile = 'webhook_db.json';
// Helper: Load DB
function loadDb($file) {
if (!file_exists($file)) return [];
$content = file_get_contents($file);
return json_decode($content, true) ?? [];
}
// Helper: Save DB
function saveDb($file, $data) {
// Suppress errors in production, or handle them
@file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
}
// 1. API: Create New UUID
// We use a specific parameter 'hook_api' to avoid collisions
if (isset($_GET['hook_api']) && $_GET['hook_api'] === 'create') {
ob_clean(); // Clear any buffer (whitespace/warnings)
header('Content-Type: application/json');
$data = loadDb($dbFile);
$newUuid = bin2hex(random_bytes(16));
$data[$newUuid] = [];
saveDb($dbFile, $data);
echo json_encode(['uuid' => $newUuid]);
exit; // Stop script execution
}
// 2. API: Get Data (Polling)
if (isset($_GET['hook_api']) && $_GET['hook_api'] === 'data' && isset($_GET['uuid'])) {
ob_clean();
header('Content-Type: application/json');
$data = loadDb($dbFile);
$uuid = $_GET['uuid'];
// If UUID doesn't exist, return empty array
echo json_encode($data[$uuid] ?? []);
exit;
}
// 3. Webhook Capture Logic
if (isset($_GET['uuid']) && !isset($_GET['view']) && !isset($_GET['hook_api'])) {
$uuid = $_GET['uuid'];
$data = loadDb($dbFile);
// Initialize array if this is the first request for this UUID
if (!isset($data[$uuid])) {
$data[$uuid] = [];
}
$headers = getallheaders();
$rawBody = file_get_contents('php://input');
$parsedBody = json_decode($rawBody, true);
if (json_last_error() !== JSON_ERROR_NONE) {
parse_str($rawBody, $parsedBody);
if (empty($parsedBody)) $parsedBody = $rawBody;
}
$newRequest = [
'id' => uniqid(),
'timestamp' => time(),
'method' => $_SERVER['REQUEST_METHOD'],
'ip' => $_SERVER['REMOTE_ADDR'],
'headers' => $headers,
'query' => $_GET,
'body' => $parsedBody,
'uri' => $_SERVER['REQUEST_URI']
];
array_unshift($data[$uuid], $newRequest);
// Limit to last 50 requests per UUID
if (count($data[$uuid]) > 50) {
$data[$uuid] = array_slice($data[$uuid], 0, 50);
}
saveDb($dbFile, $data);
http_response_code(200);
if ($_SERVER['REQUEST_METHOD'] === 'HEAD') { exit; }
echo json_encode(['status' => 'captured', 'id' => $newRequest['id']]);
exit;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Webhook Inspector | Multi-Session</title>
<style>
:root {
--primary: #3b82f6;
--primary-dark: #2563eb;
--bg-body: #f3f4f6;
--bg-card: #ffffff;
--text-main: #1f2937;
--text-muted: #6b7280;
--border: #e5e7eb;
--font-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
--font-sans: system-ui, -apple-system, sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font-sans);
background-color: var(--bg-body);
color: var(--text-main);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* --- Header --- */
header {
background: var(--bg-card);
border-bottom: 1px solid var(--border);
padding: 1rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
z-index: 10;
flex-wrap: wrap;
gap: 1rem;
}
.brand {
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 700;
font-size: 1.25rem;
color: var(--text-main);
}
.brand svg { width: 24px; height: 24px; color: var(--primary); }
.controls {
display: flex;
gap: 1rem;
align-items: center;
flex: 1;
justify-content: flex-end;
}
.btn-new {
background: var(--text-main);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
transition: background 0.2s;
white-space: nowrap;
}
.btn-new:hover { background: black; }
.webhook-display {
background: #eff6ff;
border: 1px solid #bfdbfe;
padding: 0.5rem 1rem;
border-radius: 6px;
font-family: var(--font-mono);
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 0.5rem;
max-width: 500px;
width: 100%;
}
.webhook-url {
color: var(--primary-dark);
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.copy-btn {
background: none;
border: none;
cursor: pointer;
color: var(--text-muted);
transition: color 0.2s;
display: flex;
align-items: center;
}
.copy-btn:hover { color: var(--primary); }
/* --- Layout --- */
.main-container {
display: flex;
flex: 1;
overflow: hidden;
}
/* --- Sidebar --- */
aside {
width: 350px;
background: var(--bg-card);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.sidebar-header {
padding: 1rem;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.request-list {
flex: 1;
overflow-y: auto;
list-style: none;
}
.request-item {
padding: 1rem;
border-bottom: 1px solid var(--border);
cursor: pointer;
transition: background 0.1s;
position: relative;
}
.request-item:hover { background: #f9fafb; }
.request-item.active { background: #eff6ff; border-left: 3px solid var(--primary); }
.req-top { display: flex; justify-content: space-between; margin-bottom: 0.25rem; }
.req-method { font-weight: 700; font-size: 0.8rem; text-transform: uppercase; padding: 2px 6px; border-radius: 4px; color: white; min-width: 45px; text-align: center; }
.req-time { font-size: 0.75rem; color: var(--text-muted); }
.req-path { font-size: 0.85rem; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-family: var(--font-mono); }
.empty-state {
padding: 2rem;
text-align: center;
color: var(--text-muted);
font-size: 0.9rem;
}
/* --- Main Content --- */
main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: #fff;
}
.inspector-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-muted);
}
.inspector-empty svg { width: 64px; height: 64px; margin-bottom: 1rem; opacity: 0.5; }
.inspector-content {
display: none;
flex-direction: column;
height: 100%;
}
.tabs {
display: flex;
border-bottom: 1px solid var(--border);
background: #f9fafb;
flex-shrink: 0;
}
.tab {
padding: 0.75rem 1.5rem;
font-size: 0.9rem;
cursor: pointer;
color: var(--text-muted);
border-right: 1px solid var(--border);
background: transparent;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.tab:hover { color: var(--text-main); background: #f3f4f6; }
.tab.active { color: var(--primary); border-bottom-color: var(--primary); background: white; font-weight: 500; }
.tab-panel {
flex: 1;
overflow: auto;
padding: 1.5rem;
display: none;
}
.tab-panel.active { display: block; }
.code-block {
background: #1e1e1e;
color: #d4d4d4;
padding: 1rem;
border-radius: 6px;
font-family: var(--font-mono);
font-size: 0.85rem;
overflow-x: auto;
position: relative;
white-space: pre-wrap;
}
.kv-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.kv-table th, .kv-table td {
text-align: left;
padding: 0.75rem;
border-bottom: 1px solid var(--border);
}
.kv-table th { color: var(--text-muted); font-weight: 500; width: 30%; }
.kv-table td { word-break: break-all; font-family: var(--font-mono); }
.action-bar {
padding: 1rem;
background: white;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: flex-end;
gap: 0.75rem;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 6px;
border: 1px solid var(--border);
background: white;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s;
font-size: 0.9rem;
}
.btn:hover { background: #f9fafb; border-color: #d1d5db; }
.btn-primary { background: var(--primary); color: white; border-color: var(--primary); }
.btn-primary:hover { background: var(--primary-dark); }
/* Method Colors */
.method-GET { background-color: #61affe; }
.method-POST { background-color: #49cc90; }
.method-PUT { background-color: #fca130; }
.method-DELETE { background-color: #f93e3e; }
.method-PATCH { background-color: #50e3c2; }
.method-OPTIONS { background-color: #9013fe; }
.method-HEAD { background-color: #6b7280; }
/* Toast */
.toast {
position: fixed;
bottom: 20px;
right: 20px;
background: #1f2937;
color: white;
padding: 0.75rem 1.25rem;
border-radius: 6px;
font-size: 0.9rem;
opacity: 0;
transform: translateY(20px);
transition: all 0.3s;
pointer-events: none;
z-index: 100;
}
.toast.show { opacity: 1; transform: translateY(0); }
/* Responsive */
@media (max-width: 768px) {
.main-container { flex-direction: column; }
aside { width: 100%; height: 40%; border-right: none; border-bottom: 1px solid var(--border); }
main { height: 60%; }
.header-controls { flex-direction: column; align-items: stretch; }
.webhook-display { max-width: 100%; }
}
</style>
</head>
<body>
<header>
<div class="brand">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span>HookInspect</span>
</div>
<div class="controls">
<button class="btn-new" onclick="createNewWebhook()">
<svg style="width:16px;height:16px" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
New Webhook
</button>
<div class="webhook-display">
<span id="webhookUrl" class="webhook-url">Loading...</span>
<button class="copy-btn" title="Copy URL" onclick="copyUrl()">
<svg style="width:18px;height:18px;" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012-2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
</div>
</div>
</header>
<div class="main-container">
<aside>
<div class="sidebar-header">
<strong>Incoming Requests</strong>
<button class="btn-sm" style="border:1px solid #ddd; padding:2px 6px; border-radius:4px; cursor:pointer;" onclick="clearLocalList()">Clear List</button>
</div>
<ul class="request-list" id="requestList">
<div class="empty-state">Waiting for requests...</div>
</ul>
</aside>
<main id="mainArea">
<div id="emptyInspector" class="inspector-empty">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
<p>Select a request to inspect details</p>
</div>
<div id="inspectorContent" class="inspector-content">
<div class="action-bar">
<button class="btn btn-primary" onclick="generateCurl()">
<svg style="width:16px;height:16px" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
Generate Code
</button>
</div>
<div class="tabs">
<button class="tab active" onclick="switchTab('headers')">Headers</button>
<button class="tab" onclick="switchTab('body')">Body</button>
<button class="tab" onclick="switchTab('query')">Query</button>
</div>
<div id="tab-headers" class="tab-panel active">
<table class="kv-table" id="headersTable"></table>
</div>
<div id="tab-body" class="tab-panel">
<div class="code-block" id="bodyContent"></div>
</div>
<div id="tab-query" class="tab-panel">
<table class="kv-table" id="queryTable"></table>
</div>
</div>
</main>
</div>
<div id="toast" class="toast">Copied to clipboard!</div>
<!-- Code Generation Modal -->
<div id="codeModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); z-index:50; justify-content:center; align-items:center;">
<div style="background:white; width:600px; max-width:90%; border-radius:8px; overflow:hidden; display:flex; flex-direction:column; max-height:80vh;">
<div style="padding:1rem; border-bottom:1px solid var(--border); display:flex; justify-content:space-between; align-items:center;">
<h3 style="margin:0;">Generate Code</h3>
<button onclick="document.getElementById('codeModal').style.display='none'" style="background:none;border:none;cursor:pointer;font-size:1.5rem;">×</button>
</div>
<div style="padding:0; display:flex; border-bottom:1px solid var(--border);">
<button class="tab active" id="lang-curl" onclick="setCodeLang('curl')">cURL</button>
<button class="tab" id="lang-js" onclick="setCodeLang('js')">JavaScript</button>
<button class="tab" id="lang-php" onclick="setCodeLang('php')">PHP</button>
<button class="tab" id="lang-python" onclick="setCodeLang('python')">Python</button>
</div>
<div style="flex:1; overflow:auto; background:#1e1e1e; padding:1rem;">
<pre style="margin:0; font-family:var(--font-mono); color:#d4d4d4; font-size:0.9rem;" id="codeOutput"></pre>
</div>
<div style="padding:1rem; text-align:right;">
<button class="btn btn-primary" onclick="copyCode()">Copy Code</button>
</div>
</div>
</div>
<script>
// --- State Management ---
let currentUuid = localStorage.getItem('webhook_uuid') || null;
let allRequests = [];
let currentRequestId = null;
let currentCodeLang = 'curl';
// --- Initialization ---
document.addEventListener('DOMContentLoaded', () => {
if (!currentUuid) {
createNewWebhook();
} else {
updateUI();
startPolling();
}
});
// --- Core Logic ---
async function createNewWebhook() {
try {
// FIXED: Using 'hook_api' parameter and better error handling
const response = await fetch('?hook_api=create', {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const contentType = response.headers.get("content-type");
// If we get HTML instead of JSON, alert the user with the text to help debug
if (!contentType || !contentType.includes("application/json")) {
const text = await response.text();
alert("Server Error: Expected JSON but got HTML.\n\nResponse: " + text.substring(0, 200) + "...");
throw new Error("Server did not return JSON. Check console.");
}
const result = await response.json();
if (result.uuid) {
currentUuid = result.uuid;
localStorage.setItem('webhook_uuid', currentUuid);
// Reset UI
allRequests = [];
currentRequestId = null;
document.getElementById('emptyInspector').style.display = 'flex';
document.getElementById('inspectorContent').style.display = 'none';
updateUI();
showToast("New Webhook Created!");
} else {
throw new Error("Invalid response from server");
}
} catch (e) {
console.error("Failed to create webhook", e);
showToast("Error: " + e.message);
}
}
function updateUI() {
const protocol = window.location.protocol;
const host = window.location.host;
const path = window.location.pathname;
const webhookUrl = `${protocol}//${host}${path}?uuid=${currentUuid}`;
document.getElementById('webhookUrl').textContent = webhookUrl;
renderList();
}
function startPolling() {
fetchRequests();
setInterval(fetchRequests, 2000);
}
async function fetchRequests() {
if (!currentUuid) return;
try {
const jsonRes = await fetch(`?hook_api=data&uuid=${currentUuid}&t=${Date.now()}`);
if(jsonRes.ok) {
const data = await jsonRes.json();
if (JSON.stringify(data) !== JSON.stringify(allRequests)) {
allRequests = data;
renderList();
}
}
} catch (e) {
console.error("Polling error", e);
}
}
// --- Rendering ---
function renderList() {
const listEl = document.getElementById('requestList');
listEl.innerHTML = '';
if (allRequests.length === 0) {
listEl.innerHTML = '<div class="empty-state">Waiting for requests...<br><small>Send a request to the URL above</small></div>';
return;
}
allRequests.forEach(req => {
const li = document.createElement('li');
li.className = `request-item ${currentRequestId === req.id ? 'active' : ''}`;
li.onclick = () => selectRequest(req.id);
const date = new Date(req.timestamp * 1000);
const timeStr = date.toLocaleTimeString();
li.innerHTML = `
<div class="req-top">
<span class="req-method method-${req.method}">${req.method}</span>
<span class="req-time">${timeStr}</span>
</div>
<div class="req-path">${req.ip} - ${req.id}</div>
`;
listEl.appendChild(li);
});
}
function selectRequest(id) {
currentRequestId = id;
renderList();
const req = allRequests.find(r => r.id === id);
if (!req) return;
document.getElementById('emptyInspector').style.display = 'none';
document.getElementById('inspectorContent').style.display = 'flex';
switchTab('headers');
renderTable('headersTable', req.headers);
renderTable('queryTable', req.query);
const bodyEl = document.getElementById('bodyContent');
if (typeof req.body === 'object') {
bodyEl.textContent = JSON.stringify(req.body, null, 2);
} else {
bodyEl.textContent = req.body || '(Empty Body)';
}
}
function renderTable(tableId, data) {
const table = document.getElementById(tableId);
table.innerHTML = '';
if (!data || (typeof data === 'object' && Object.keys(data).length === 0)) {
table.innerHTML = '<tr><td colspan="2" style="color:#aaa; text-align:center;">No data available</td></tr>';
return;
}
for (const [key, value] of Object.entries(data)) {
const tr = document.createElement('tr');
tr.innerHTML = `
<th>${escapeHtml(key)}</th>
<td>${escapeHtml(value)}</td>
`;
table.appendChild(tr);
}
}
// --- Tabs ---
function switchTab(tabName) {
document.querySelectorAll('.tabs .tab').forEach(t => t.classList.remove('active'));
const tabs = ['headers', 'body', 'query'];
const btns = document.querySelectorAll('.tabs .tab');
btns[tabs.indexOf(tabName)].classList.add('active');
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.getElementById('tab-' + tabName).classList.add('active');
}
// --- Code Generation ---
function generateCurl() {
if (!currentRequestId) return;
const modal = document.getElementById('codeModal');
modal.style.display = 'flex';
setCodeLang('curl');
}
function setCodeLang(lang) {
currentCodeLang = lang;
const langs = ['curl', 'js', 'php', 'python'];
langs.forEach(l => {
const btn = document.getElementById('lang-' + l);
if(l === lang) btn.classList.add('active');
else btn.classList.remove('active');
});
const req = allRequests.find(r => r.id === currentRequestId);
if (!req) return;
const outputEl = document.getElementById('codeOutput');
let code = '';
const protocol = window.location.protocol;
const host = window.location.host;
const path = window.location.pathname;
const webhookUrl = `${protocol}//${host}${path}?uuid=${currentUuid}`;
const headers = req.headers || {};
let bodyStr = '';
if (typeof req.body === 'object') {
bodyStr = JSON.stringify(req.body);
} else if (req.body) {
bodyStr = String(req.body);
}
if (lang === 'curl') {
code = `curl -X ${req.method} "${webhookUrl}" \\\n`;
for (const [k, v] of Object.entries(headers)) {
code += ` -H "${k}: ${escapeCmd(v)}" \\\n`;
}
if (['POST', 'PUT', 'PATCH'].includes(req.method) && bodyStr) {
let safeBody = bodyStr.replace(/'/g, "'\\''");
code += ` -d '${safeBody}'`;
}
} else if (lang === 'js') {
code = `fetch("${webhookUrl}", {\n`;
code += ` method: "${req.method}",\n`;
code += ` headers: {\n`;
for (const [k, v] of Object.entries(headers)) {
code += ` "${k}": "${escapeJs(v)}",\n`;
}
code += ` },\n`;
if (['POST', 'PUT', 'PATCH'].includes(req.method) && bodyStr) {
code += ` body: ${typeof req.body === 'object' ? JSON.stringify(req.body, null, 2) : `"${escapeJs(bodyStr)}"`}\n`;
}
code += `});`;
} else if (lang === 'php') {
code = `$ch = curl_init();\n`;
code += `curl_setopt($ch, CURLOPT_URL, "${webhookUrl}");\n`;
code += `curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "${req.method}");\n`;
code += `curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n`;
let hArr = [];
for (const [k, v] of Object.entries(headers)) {
hArr.push(`"${k}: ${v}"`);
}
code += `curl_setopt($ch, CURLOPT_HTTPHEADER, [${hArr.join(', ')}]);\n`;
if (['POST', 'PUT', 'PATCH'].includes(req.method) && bodyStr) {
let phpBody = bodyStr.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
code += `curl_setopt($ch, CURLOPT_POSTFIELDS, '${phpBody}');\n`;
}
code += `\n$response = curl_exec($ch);\ncurl_close($ch);`;
} else if (lang === 'python') {
code = `import requests\n\n`;
code += `url = "${webhookUrl}"\n`;
code += `headers = {\n`;
for (const [k, v] of Object.entries(headers)) {
code += ` "${k}": "${escapePy(v)}",\n`;
}
code += `}\n`;
if (['POST', 'PUT', 'PATCH'].includes(req.method) && bodyStr) {
code += `data = ${typeof req.body === 'object' ? JSON.stringify(req.body, null, 4) : `"${escapePy(bodyStr)}"`}\n`;
}
code += `\nresponse = requests.request("${req.method}", url, headers=headers${['POST', 'PUT', 'PATCH'].includes(req.method) && bodyStr ? ', data=data' : ''})\nprint(response.text)`;
}
outputEl.textContent = code;
}
function copyCode() {
const code = document.getElementById('codeOutput').textContent;
navigator.clipboard.writeText(code);
showToast("Code copied!");
}
// --- Utilities ---
function copyUrl() {
const url = document.getElementById('webhookUrl').textContent;
navigator.clipboard.writeText(url);
showToast("Webhook URL copied!");
}
function clearLocalList() {
allRequests = [];
renderList();
document.getElementById('emptyInspector').style.display = 'flex';
document.getElementById('inspectorContent').style.display = 'none';
showToast("Local list cleared");
}
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 3000);
}
function escapeHtml(text) {
if (typeof text !== 'string') return text;
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
}
function escapeCmd(str) { return String(str).replace(/"/g, '\\"'); }
function escapeJs(str) { return String(str).replace(/\\/g, '\\\\').replace(/"/g, '\\"'); }
function escapePy(str) { return String(str).replace(/\\/g, '\\\\').replace(/"/g, '\\"'); }
</script>
</body>
</html>
0 Comments