Building a Self-Hosted Webhook Inspector with PHP and Vanilla JS

by | Feb 3, 2026 | Projects | 0 comments

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:

  1. The Collector (API Mode): When the script receives a request with a specific uuid parameter, it captures the headers, body, and query parameters, saves them to a local JSON file, and responds with 200 OK.
  2. 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 $_GET parameters 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.
  • 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 Request

Developer 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:

  1. Denial of Service (DoS): An attacker could spam the endpoint, filling the disk with JSON data or spiking I/O usage.
  2. 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;">&times;</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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
    }

    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>

Written by

Related Posts

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *