Free webhook tester for you and your agent
Welcome to another official UVF IT guide where we roll up our sleeves and build a tiny clone of Hookdeck Inspect – a free webhook tester you can run locally in minutes.
In this tutorial you’ll scaffold a Node.js app, store incoming webhook payloads in SQLite, expose a minimal API to list them, and call the real Hookdeck Inspect endpoint for comparison – all using free‑tier tools.
.env file; without it the external inspect call will fail.We start by creating a fresh npm project so we have a package.json to track dependencies. Express will serve our HTTP endpoints, better‑sqlite3 gives us a fast embedded DB, dotenv loads secret keys, and nodemon keeps the server alive while we code.
Installing everything locally means the clone runs on any machine without extra services – just Node and npm.
mkdir webhook-tester && cd webhook-tester
npm init -y
npm install express better-sqlite3 dotenv
npm install --save-dev nodemon
# Add a dev script for live reload
npx json -I -f package.json -e "this.scripts={...this.scripts,dev:'nodemon index.js'}"Next we create a tiny SQLite database that stores each incoming webhook as raw JSON together with the time it arrived. This keeps the implementation simple yet gives us persistent storage without a separate server.
The db.js module will be required by our Express routes so they can insert and query payloads.
db.js
const Database = require('better-sqlite3');
const path = require('path');
// SQLite file lives in the project root
const db = new Database(path.resolve(__dirname, 'webhooks.db'));
// Ensure the table exists
const init = `CREATE TABLE IF NOT EXISTS payloads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
received_at TEXT NOT NULL,
body TEXT NOT NULL
);`;
db.exec(init);
module.exports = db;In this step we create two core endpoints: a POST /webhook that accepts any JSON payload and stores it locally, and a GET /events that returns all stored entries as a JSON array. The storage uses a tiny low‑db JSON file so the data survives across restarts without any external database. If you provide a Hookdeck token in the .env file, each incoming payload is also forwarded to Hookdeck’s Inspect API for remote inspection, but the feature is optional and harmless when omitted.
We keep the code small and readable: Express handles routing, body‑parser parses JSON, and low‑db provides a simple db.get('events').push(...).write() API. Errors are caught and reported with a 500 status, ensuring the server never crashes on malformed input.
index.js
require('dotenv').config();
const express = require('express');
const fetch = require('node-fetch');
const { Low, JSONFile } = require('lowdb');
const path = require('path');
const app = express();
app.use(express.json());
// Initialize lowdb
const file = path.join(__dirname, 'db.json');
const adapter = new JSONFile(file);
const db = new Low(adapter);
(async () => { await db.read(); db.data ||= { events: [] }; await db.write(); })();
// POST /webhook – capture payload
app.post('/webhook', async (req, res) => {
try {
const payload = req.body;
const event = { id: Date.now(), payload, received_at: new Date().toISOString() };
db.data.events.push(event);
await db.write();
// Optional Hookdeck forward
if (process.env.HOOKDECK_TOKEN) {
await fetch('https://api.hookdeck.com/v1/inspect', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.HOOKDECK_TOKEN}`,
},
body: JSON.stringify(payload),
});
}
res.status(200).json({ status: 'captured', id: event.id });
} catch (e) {
console.error(e);
res.status(500).json({ error: 'internal server error' });
}
});
// GET /events – list stored entries
app.get('/events', async (req, res) => {
await db.read();
res.json(db.data.events);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));We use nodemon so the server restarts automatically whenever you change a file – perfect for rapid iteration. Make sure you have a .env file at the project root; at minimum it can be empty, but you may add HOOKDECK_TOKEN=your_token to enable remote inspection.
Start the app with the provided command; you should see a console message confirming the server is listening on port 3000. If any dependency is missing, npm will install it automatically when you run the install step earlier in the guide.
npm install express lowdb node-fetch dotenv
npm install -D nodemon
npx nodemon index.jsNow that the server is running, send a sample webhook using curl. The POST request should return a JSON confirmation and the payload will be saved to db.json. Then retrieve the stored events with a simple GET request – you should see an array containing the payload you just sent.
From here you can extend the tester: add pagination parameters to /events, build a tiny React UI to display events in real time, or protect the endpoints with an API‑key header. Each extension builds on the same low‑db foundation, keeping the project lightweight and easy to understand.
curl -X POST http://localhost:3000/webhook \
-H "Content-Type: application/json" \
-d '{"test":"value","count":42}'
curl http://localhost:3000/events| Check | Command / Why it matters |
|---|---|
| Server starts without errors | npx nodemon index.js shows "Server running on http://localhost:3000" |
| POST request returns 200 | curl -X POST http://localhost:3000/webhook -H "Content-Type: application/json" -d '{"a":1}' exits with status 0 and prints a JSON response |
| GET /events shows JSON array with your payload | curl http://localhost:3000/events prints [{"id":...,"payload":{"a":1},"received_at":"..."}] |
You now have a functional webhook tester. Add features like pagination, a React UI, or secure the endpoints with a simple API key to get closer to the full Hookdeck experience.