Source Code
<div class="container py-5 d-flex justify-content-center">
<div class="card bswh-card">
<div class="card-body p-3">
<h6 class="fw-bold mb-2">Webhook deliveries</h6>
<ul class="list-unstyled mb-0" id="bswhList"></ul>
</div>
</div>
</div>.bswh-card { width: 440px; max-width: 100%; border: 1px solid #eceef1; border-radius: 14px; }
.bswh-row { border-bottom: 1px solid #f1f2f5; }
.bswh-row:last-child { border-bottom: none; }
.bswh-head { display: flex; justify-content: space-between; align-items: center; padding: 9px 4px; cursor: pointer; }
.bswh-event { font: 700 12.5px ui-monospace, Menlo, Consolas, monospace; }
.bswh-time { font-size: 11px; color: #9ca3af; }
.bswh-body { display: none; padding: 0 4px 10px; }
.bswh-body.bswh-open { display: block; }
.bswh-payload {
margin: 6px 0 8px; padding: 8px 10px; background: #14151a; color: #e1e4e8;
border-radius: 8px; font: 11.5px/1.5 ui-monospace, Menlo, Consolas, monospace; overflow-x: auto; white-space: pre;
}const EVENTS = [
{ event: 'invoice.paid', status: 200, time: '2 min ago', payload: '{ "id": "in_1a2b", "amount": 4900, "currency": "usd" }' },
{ event: 'customer.subscription.updated', status: 500, time: '14 min ago', payload: '{ "id": "sub_9x8y", "status": "past_due" }' },
{ event: 'checkout.session.completed', status: 200, time: '1 hr ago', payload: '{ "id": "cs_7f6e", "customer": "cus_44kd" }' },
{ event: 'invoice.payment_failed', status: 404, time: '3 hr ago', payload: '{ "id": "in_5c4d", "reason": "endpoint not found" }' },
];
const list = document.getElementById('bswhList');
let events = EVENTS.map((e, id) => ({ ...e, id, open: false }));
function toneFor(status) {
if (status < 300) return 'success';
if (status < 500) return 'warning';
return 'danger';
}
function render() {
list.innerHTML = events.map(ev =>
'<li class="bswh-row" data-id="' + ev.id + '">' +
'<div class="bswh-head" data-toggle="' + ev.id + '">' +
'<div><span class="badge text-bg-' + toneFor(ev.status) + ' me-2">' + ev.status + '</span><span class="bswh-event">' + ev.event + '</span></div>' +
'<span class="bswh-time">' + ev.time + '</span>' +
'</div>' +
'<div class="bswh-body' + (ev.open ? ' bswh-open' : '') + '">' +
'<pre class="bswh-payload mb-0">' + ev.payload + '</pre>' +
(ev.status >= 400 ? '<button type="button" class="btn btn-sm btn-outline-danger" data-retry="' + ev.id + '">Retry delivery</button>' : '') +
'</div>' +
'</li>'
).join('');
}
list.addEventListener('click', e => {
const head = e.target.closest('.bswh-head');
const retryBtn = e.target.closest('[data-retry]');
if (retryBtn) {
const ev = events.find(x => x.id === Number(retryBtn.dataset.retry));
retryBtn.textContent = 'Retrying...';
retryBtn.disabled = true;
setTimeout(() => {
ev.status = 200;
ev.time = 'Just now (retried)';
render();
}, 900);
return;
}
if (head) {
const ev = events.find(x => x.id === Number(head.dataset.toggle));
ev.open = !ev.open;
render();
}
});
render();Bootstrap Webhook Event Viewer — Free HTML CSS JS Snippet
Bootstrap Webhook Event Viewer · Dashboards · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Bootstrap Webhook Event Viewer — HTML, CSS & JavaScript

Each delivery tracks its own open boolean, toggled independently by clicking its row header — expanding one delivery's payload has no effect on any other row's expanded state, since render() derives every row's visibility purely from that row's own open value rather than a single "currently expanded id" that would force closing one to open another.
toneFor(status) reuses the same status-family logic as bootstrap-http-status-badge — a 2xx delivery badges green, 4xx amber, 5xx red — so a user can scan the list and immediately spot which deliveries need attention without reading every status code individually.
Retry only appears on a row whose status >= 400, since retrying a delivery that already succeeded is a meaningless action. Clicking it disables the button, shows "Retrying...", and after a simulated delay mutates that specific event's status to 200 and updates its timestamp — a real implementation would replace that timeout with an actual re-delivery API call, treating a successful response the same way this demo treats the simulated one.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Hand this snippet to an AI coding assistant like Claude and ask it to add response headers alongside the payload in the expanded view, or to add a filter toggle showing only failed deliveries, making it faster to find and retry everything that needs attention.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a Bootstrap 5.3 webhook delivery event viewer, using the real Bootstrap CDN framework (bootstrap.min.css and bootstrap.bundle.min.js), not custom CSS made to resemble it.
Requirements:
- A list of at least 4 sample webhook deliveries, each with an event name, an HTTP status code shown as a colored badge (green for 2xx, amber for 4xx, red for 5xx), and a relative timestamp.
- Clicking a delivery's row expands it in place to reveal its raw JSON payload in a monospace block; each row's expanded state must be tracked independently, so multiple rows can be expanded at once without affecting each other.
- A "Retry delivery" button must appear only inside the expanded view of deliveries with a status of 400 or higher — never on an already-successful delivery.
- Clicking Retry must disable the button, show a "Retrying..." state, and after a short simulated delay update that specific delivery's status to a success code and its timestamp, re-rendering it in place without a Retry button anymore.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Load the snippetFour webhook deliveries list, each with a status badge, event name, and relative time — none expanded yet.
- 2Click the "invoice.paid" rowIt expands to reveal its raw JSON payload; no Retry button shows, since it already succeeded.
- 3Click the "customer.subscription.updated" row (500 status)It expands showing its payload and a "Retry delivery" button.
- 4Click "Retry delivery"The button disables and shows "Retrying...", then the row updates to a 200 status with an updated timestamp.
- 5Expand a different row while the first stays expandedBoth rows stay independently expanded — opening one never closes another.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Yes — each row's open state is tracked independently, so expanding one has no effect on any other row; there's no artificial limit to a single expanded row at a time.
It only renders when a delivery's status is 400 or above — retrying an already-successful (2xx) delivery has no meaningful action to perform, so the button is withheld entirely rather than shown disabled.
This demo simulates the outcome with a timeout; a real implementation should call your actual re-delivery API endpoint and update the row's status based on that request's genuine result, success or failure.
Yes. Keep the events array (including each event's open boolean) in component state, toggle a specific event's open flag immutably on row click, and update its status/time fields the same way on a successful retry.