GuidesWebhook Apps
Custom Hook Flow Implementation
Build a full webhook management experience in your product using Wacht hooks.
Custom Hook Flow Implementation
Use custom hooks when webhook UX is part of your core product, not an embedded module.
Best for:
- Endpoint + delivery management inside your existing settings IA.
- Product-specific controls (approval gates, feature tiers, tenant-level restrictions).
- Unified operator workflow (endpoint actions + replay + analytics in one screen).
End-to-end architecture
- Backend issues short-lived
webhook_app_accessticket after RBAC checks. - Frontend exchanges ticket with
useWebhookAppSession(ticket). - Frontend manages endpoints using session hook mutations.
- Frontend reads deliveries/analytics/timeseries with dedicated hooks.
1) Backend ticket endpoint
import express from "express";
import { WachtClient } from "@wacht/backend";
const app = express();
app.use(express.json());
const wacht = new WachtClient({ apiKey: process.env.WACHT_BACKEND_API_KEY! });
app.post("/api/settings/webhooks/embed-ticket", async (req, res) => {
const user = req.user as { canManageWebhooks?: boolean } | undefined;
const deploymentId = req.body?.deploymentId as string | undefined;
if (!user) return res.status(401).json({ error: "Unauthorized" });
if (!user.canManageWebhooks) return res.status(403).json({ error: "Forbidden" });
if (!deploymentId) return res.status(400).json({ error: "deploymentId is required" });
const ticket = await wacht.post<{ ticket: string; expires_at: number }>("/session/tickets", {
ticket_type: "webhook_app_access",
webhook_app_slug: `wh_${deploymentId}`,
expires_in: 120,
});
res.json(ticket);
});2) Frontend session gate + endpoint lifecycle
import { useEffect, useState } from "react";
import { useWebhookAppSession, useWebhookEndpoints } from "@wacht/nextjs";
export function WebhookSettings({ deploymentId }: { deploymentId: string }) {
const [ticket, setTicket] = useState<string | null>(null);
useEffect(() => {
void fetch("/api/settings/webhooks/embed-ticket", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ deploymentId }),
})
.then((r) => r.json())
.then((payload: { ticket: string }) => setTicket(payload.ticket));
}, [deploymentId]);
const session = useWebhookAppSession(ticket);
const endpoints = useWebhookEndpoints();
if (session.sessionLoading) return <div>Loading webhook access...</div>;
if (!session.hasSession) return <div>Access required</div>;
if (session.sessionError) return <div>Could not establish webhook session</div>;
return (
<section>
<h2>{session.webhookApp?.name}</h2>
<button
onClick={() =>
session.createEndpoint({
url: "https://customer.example.com/hooks/wacht",
description: "Primary endpoint",
subscribed_events: ["invoice.paid", "invoice.failed"],
})
}
>
Add endpoint
</button>
{endpoints.endpoints.map((endpoint) => (
<div key={endpoint.id}>
<span>{endpoint.url}</span>
<button
onClick={() =>
session.testEndpoint(endpoint.id, {
event_name: "invoice.paid",
payload: { invoice_id: "inv_123" },
})
}
>
Test
</button>
<button onClick={() => session.deleteEndpoint(endpoint.id)}>Delete</button>
</div>
))}
</section>
);
}3) Deliveries + replay + analytics block
import { useWebhookDeliveries, useWebhookAnalytics, useWebhookTimeseries } from "@wacht/nextjs";
export function WebhookOpsPanel() {
const deliveries = useWebhookDeliveries({ status: "failed", limit: 25 });
const analytics = useWebhookAnalytics({ fields: ["success_rate", "failed", "total_deliveries"] });
const timeseries = useWebhookTimeseries({ interval: "hour" });
if (deliveries.loading || analytics.loading || timeseries.loading) return <div>Loading webhook operations...</div>;
return (
<section>
<div>Failed deliveries: {deliveries.deliveries.length}</div>
<div>Success rate: {analytics.analytics?.success_rate ?? 0}%</div>
<div>Timeseries points: {timeseries.timeseries.length}</div>
</section>
);
}4) Replay workflow
const replay = await session.replayDelivery({
status: "failed",
start_date: "2026-04-01T00:00:00Z",
end_date: "2026-04-01T23:59:59Z",
});
const taskId = replay.data.task_id;
if (taskId) {
await session.fetchReplayTaskStatus({ taskId });
}Production implementation notes
- Keep ticket issuance server-only with explicit tenant/RBAC checks.
- Validate endpoint URLs and show clear warnings before destructive operations.
- Enforce idempotent receiver requirement in customer docs.
- Limit replay ranges and add operator audit logs for replay/cancel actions.
- Refresh endpoint/delivery/analytics widgets after lifecycle mutations.
Validation checklist
- Unauthorized users cannot acquire
webhook_app_accesstickets. - Endpoint create/update/delete/test works with tenant isolation.
- Failed delivery replay works and task status updates are visible.
- Analytics and timeseries reflect delivery state transitions.