[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build a Webhook Event Log

Build a Webhook Event Log

Build a webhook event log with searchable delivery history, payload inspection, replay functionality, filtering, and export for debugging webhook integrations.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
$

The Problem

Alex leads integrations at a 20-person SaaS. Customers ask "did you send the webhook for order #12345?" and nobody can answer quickly. Failed deliveries have no record of what was sent or what error the customer's server returned. When a customer changes their webhook URL, they want to replay recent events to the new endpoint. Debugging webhook issues requires SSH-ing into servers and grep-ing logs. They need a webhook event log: searchable history, full payload inspection, delivery status with response details, replay capability, and customer self-service.

Step 1: Build the Event Log

typescript
import { pool } from "../db";
import { Redis } from "ioredis";
import { createHmac, randomBytes } from "node:crypto";
const redis = new Redis(process.env.REDIS_URL!);

interface WebhookEvent {
  id: string;
  eventType: string;
  endpointId: string;
  customerId: string;
  payload: any;
  deliveryStatus: "pending" | "delivered" | "failed" | "retrying";
  attempts: Array<{ timestamp: string; statusCode: number | null; responseBody: string | null; error: string | null; latencyMs: number }>;
  createdAt: string;
}

// Log webhook event
export async function logEvent(params: { eventType: string; endpointId: string; customerId: string; payload: any }): Promise<string> {
  const id = `evt-${randomBytes(8).toString("hex")}`;
  await pool.query(
    `INSERT INTO webhook_events (id, event_type, endpoint_id, customer_id, payload, delivery_status, attempts, created_at)
     VALUES ($1, $2, $3, $4, $5, 'pending', '[]', NOW())`,
    [id, params.eventType, params.endpointId, params.customerId, JSON.stringify(params.payload)]
  );
  return id;
}

// Record delivery attempt
export async function recordAttempt(eventId: string, attempt: { statusCode: number | null; responseBody: string | null; error: string | null; latencyMs: number }): Promise<void> {
  const { rows: [event] } = await pool.query("SELECT attempts FROM webhook_events WHERE id = $1", [eventId]);
  if (!event) return;
  const attempts = JSON.parse(event.attempts);
  attempts.push({ ...attempt, timestamp: new Date().toISOString() });
  const status = attempt.statusCode && attempt.statusCode >= 200 && attempt.statusCode < 300 ? "delivered" : attempts.length >= 5 ? "failed" : "retrying";
  await pool.query("UPDATE webhook_events SET attempts = $2, delivery_status = $3 WHERE id = $1", [eventId, JSON.stringify(attempts), status]);
}

// Search events
export async function searchEvents(params: { customerId?: string; eventType?: string; status?: string; startDate?: string; endDate?: string; limit?: number; offset?: number }): Promise<{ events: WebhookEvent[]; total: number }> {
  let sql = "SELECT * FROM webhook_events WHERE 1=1";
  const queryParams: any[] = [];
  let idx = 1;
  if (params.customerId) { sql += ` AND customer_id = $${idx}`; queryParams.push(params.customerId); idx++; }
  if (params.eventType) { sql += ` AND event_type = $${idx}`; queryParams.push(params.eventType); idx++; }
  if (params.status) { sql += ` AND delivery_status = $${idx}`; queryParams.push(params.status); idx++; }
  if (params.startDate) { sql += ` AND created_at >= $${idx}`; queryParams.push(params.startDate); idx++; }
  if (params.endDate) { sql += ` AND created_at <= $${idx}`; queryParams.push(params.endDate); idx++; }

  const countSql = sql.replace("SELECT *", "SELECT COUNT(*) as count");
  const { rows: [{ count }] } = await pool.query(countSql, queryParams);

  sql += ` ORDER BY created_at DESC LIMIT $${idx} OFFSET $${idx + 1}`;
  queryParams.push(params.limit || 50, params.offset || 0);
  const { rows } = await pool.query(sql, queryParams);

  return { events: rows.map((r: any) => ({ ...r, payload: JSON.parse(r.payload), attempts: JSON.parse(r.attempts) })), total: parseInt(count) };
}

// Replay event to current or new endpoint
export async function replayEvent(eventId: string, targetUrl?: string): Promise<{ success: boolean; statusCode: number; responseBody: string }> {
  const { rows: [event] } = await pool.query("SELECT * FROM webhook_events WHERE id = $1", [eventId]);
  if (!event) throw new Error("Event not found");

  let url = targetUrl;
  if (!url) {
    const { rows: [endpoint] } = await pool.query("SELECT url, secret FROM webhook_endpoints WHERE id = $1", [event.endpoint_id]);
    if (!endpoint) throw new Error("Endpoint not found");
    url = endpoint.url;
  }

  const payload = JSON.parse(event.payload);
  const start = Date.now();
  const response = await fetch(url!, {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-Webhook-ID": eventId, "X-Webhook-Replay": "true" },
    body: JSON.stringify(payload),
    signal: AbortSignal.timeout(10000),
  });
  const responseBody = await response.text();
  const latencyMs = Date.now() - start;

  await recordAttempt(eventId, { statusCode: response.status, responseBody: responseBody.slice(0, 1000), error: null, latencyMs });

  return { success: response.ok, statusCode: response.status, responseBody: responseBody.slice(0, 1000) };
}

// Bulk replay (e.g., after customer changes URL)
export async function bulkReplay(customerId: string, options: { eventTypes?: string[]; since?: string; targetUrl?: string }): Promise<{ replayed: number; failed: number }> {
  let sql = "SELECT id FROM webhook_events WHERE customer_id = $1";
  const params: any[] = [customerId];
  let idx = 2;
  if (options.since) { sql += ` AND created_at >= $${idx}`; params.push(options.since); idx++; }
  if (options.eventTypes?.length) { sql += ` AND event_type = ANY($${idx})`; params.push(options.eventTypes); idx++; }
  sql += " ORDER BY created_at ASC LIMIT 100";

  const { rows } = await pool.query(sql, params);
  let replayed = 0, failed = 0;
  for (const row of rows) {
    try { await replayEvent(row.id, options.targetUrl); replayed++; } catch { failed++; }
  }
  return { replayed, failed };
}

// Customer-facing event log
export async function getCustomerEventLog(customerId: string, limit: number = 50): Promise<WebhookEvent[]> {
  return (await searchEvents({ customerId, limit })).events;
}

Results

  • "Did you send the webhook?" — answered in 2 seconds — search by order ID, event type, or date; full payload and delivery attempts visible; no SSH needed
  • Failed delivery debugging — see exact HTTP status code and response body from customer's server; "your server returned 500 with 'invalid JSON'" — customer fixes their handler
  • Replay after URL change — customer changes webhook URL → bulk replay last 7 days of events to new URL; no lost events during migration
  • Self-service event log — customer dashboard shows all webhook deliveries with status; reduces support tickets by 40%
  • Full audit trail — every delivery attempt logged with timestamp, status, response, and latency; compliance and debugging covered