Blocks
Webhooks Block
Developer UIA responsive developer operations console for endpoint setup, event subscriptions, signing-secret rotation, delivery inspection, and replay workflow.
Developer Experience
Webhook operations console
Copy this into a developer dashboard, integration settings page, platform admin console, or internal delivery monitor. Replace the sample endpoint, event, and delivery arrays with your webhook API data.
1440px
<script setup>
import { computed, ref, watch } from 'vue';
import { DomButton, DomCard, DomNativeSelect, DomTabs, DomTextInput, DomToggle } from '../../../lib/vue';
const environmentOptions = [
{ label: 'Production', value: 'Production' },
{ label: 'Staging', value: 'Staging' },
{ label: 'Development', value: 'Development' },
];
const tabs = [
{ key: 'events', label: 'Events' },
{ key: 'deliveries', label: 'Deliveries' },
{ key: 'security', label: 'Security' },
];
const eventGroups = [
{
title: 'Customers',
events: [
{ key: 'customer.created', label: 'Customer created', description: 'A customer profile is created.' },
{ key: 'customer.updated', label: 'Customer updated', description: 'A customer profile changes.' },
],
},
{
title: 'Billing',
events: [
{ key: 'invoice.paid', label: 'Invoice paid', description: 'An invoice is paid successfully.' },
{ key: 'invoice.payment_failed', label: 'Payment failed', description: 'A payment attempt needs attention.' },
],
},
{
title: 'Subscriptions',
events: [
{ key: 'subscription.created', label: 'Subscription created', description: 'A subscription becomes active.' },
{ key: 'subscription.updated', label: 'Subscription updated', description: 'Plan, quantity, or status changes.' },
],
},
];
const endpoints = [
{
id: 'we_prod_platform',
name: 'Production platform',
url: 'https://api.acme.example/webhooks/dom',
environment: 'Production',
status: 'Healthy',
apiVersion: '2026-06-01',
secretPrefix: 'whsec_4A7K',
successRate: 99.6,
latencyMs: 318,
failures: 2,
events: ['customer.created', 'invoice.paid', 'subscription.updated'],
attempts: [
{ id: 'evt_1092', event: 'invoice.paid', status: 'Delivered', responseCode: 200, latencyMs: 241, time: '2 minutes ago' },
{ id: 'evt_1091', event: 'customer.created', status: 'Delivered', responseCode: 200, latencyMs: 289, time: '11 minutes ago' },
{ id: 'evt_1088', event: 'invoice.payment_failed', status: 'Failed', responseCode: 500, latencyMs: 10000, time: '28 minutes ago' },
],
},
{
id: 'we_prod_crm',
name: 'CRM enrichment',
url: 'https://hooks.customerops.example/dom',
environment: 'Production',
status: 'Needs review',
apiVersion: '2026-04-15',
secretPrefix: 'whsec_9LM2',
successRate: 96.8,
latencyMs: 842,
failures: 8,
events: ['customer.created', 'customer.updated'],
attempts: [
{ id: 'evt_1077', event: 'customer.updated', status: 'Retrying', responseCode: 429, latencyMs: 1200, time: '44 minutes ago' },
{ id: 'evt_1072', event: 'customer.created', status: 'Delivered', responseCode: 200, latencyMs: 612, time: '1 hour ago' },
{ id: 'evt_1066', event: 'customer.updated', status: 'Failed', responseCode: 401, latencyMs: 164, time: '3 hours ago' },
],
},
{
id: 'we_stage_checkout',
name: 'Checkout staging',
url: 'https://staging.acme.example/webhooks/dom',
environment: 'Staging',
status: 'Testing',
apiVersion: '2026-06-01',
secretPrefix: 'whsec_TEST',
successRate: 91.4,
latencyMs: 511,
failures: 5,
events: ['invoice.paid', 'invoice.payment_failed', 'subscription.created'],
attempts: [
{ id: 'evt_test_211', event: 'subscription.created', status: 'Delivered', responseCode: 200, latencyMs: 333, time: '7 minutes ago' },
{ id: 'evt_test_210', event: 'invoice.payment_failed', status: 'Failed', responseCode: 404, latencyMs: 88, time: '19 minutes ago' },
{ id: 'evt_test_208', event: 'invoice.paid', status: 'Delivered', responseCode: 200, latencyMs: 305, time: '1 hour ago' },
],
},
{
id: 'we_dev_tunnel',
name: 'Local tunnel',
url: 'https://dev-tunnel.example/webhooks',
environment: 'Development',
status: 'Testing',
apiVersion: '2026-06-01',
secretPrefix: 'whsec_DEV',
successRate: 88.2,
latencyMs: 190,
failures: 3,
events: ['customer.created', 'invoice.paid'],
attempts: [
{ id: 'evt_dev_044', event: 'customer.created', status: 'Delivered', responseCode: 200, latencyMs: 145, time: '4 minutes ago' },
{ id: 'evt_dev_043', event: 'invoice.paid', status: 'Retrying', responseCode: 502, latencyMs: 900, time: '14 minutes ago' },
{ id: 'evt_dev_041', event: 'invoice.paid', status: 'Failed', responseCode: 0, latencyMs: 10000, time: '52 minutes ago' },
],
},
];
const selectedEnvironment = ref('Production');
const selectedEndpointId = ref(endpoints[0].id);
const activeTab = ref('events');
const endpointUrl = ref(endpoints[0].url);
const apiVersion = ref(endpoints[0].apiVersion);
const selectedEvents = ref([...endpoints[0].events]);
const deliveryFilter = ref('All');
const showSecret = ref(false);
const sendTestEvent = ref(true);
const filteredEndpoints = computed(() => endpoints.filter((endpoint) => endpoint.environment === selectedEnvironment.value));
const selectedEndpoint = computed(() => endpoints.find((endpoint) => endpoint.id === selectedEndpointId.value) || filteredEndpoints.value[0] || endpoints[0]);
const filteredAttempts = computed(() => {
if (deliveryFilter.value === 'All') return selectedEndpoint.value.attempts;
return selectedEndpoint.value.attempts.filter((attempt) => attempt.status === deliveryFilter.value);
});
const subscribedCount = computed(() => selectedEvents.value.length);
const visibleSecret = computed(() => showSecret.value ? `${selectedEndpoint.value.secretPrefix}_rV4mH6kL1sN7xT3c` : `${selectedEndpoint.value.secretPrefix}****************`);
const healthTone = computed(() => selectedEndpoint.value.status === 'Healthy' ? 'text-emerald-700 dark:text-emerald-300' : 'text-warning');
watch(selectedEnvironment, () => {
const firstEndpoint = filteredEndpoints.value[0];
if (firstEndpoint) selectEndpoint(firstEndpoint);
});
function selectEndpoint(endpoint) {
selectedEndpointId.value = endpoint.id;
endpointUrl.value = endpoint.url;
apiVersion.value = endpoint.apiVersion;
selectedEvents.value = [...endpoint.events];
activeTab.value = 'events';
}
function toggleEvent(eventKey) {
selectedEvents.value = selectedEvents.value.includes(eventKey)
? selectedEvents.value.filter((key) => key !== eventKey)
: [...selectedEvents.value, eventKey];
}
function deliveryClasses(status) {
return {
Delivered: 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-300',
Retrying: 'bg-warning/15 text-warning',
Failed: 'bg-destructive/15 text-destructive',
}[status] || 'bg-secondary text-muted-fg';
}
</script>
<template>
<div class="w-full overflow-hidden rounded-3xl border border-border bg-background text-fg shadow-2xl shadow-black/10">
<header class="flex flex-wrap items-center justify-between gap-3 border-b border-border skin-raised px-4 py-4 sm:px-5">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-muted-fg">Developer operations</p>
<h3 class="mt-1 text-xl font-semibold tracking-tight">Webhooks</h3>
</div>
<div class="flex flex-wrap items-center gap-2">
<DomNativeSelect v-model="selectedEnvironment" :options="environmentOptions" class="w-40" />
<DomButton size="sm">
<svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
</svg>
New endpoint
</DomButton>
</div>
</header>
<div class="grid min-h-[45rem] lg:grid-cols-[18rem_minmax(0,1fr)_19rem]">
<aside class="border-b border-border skin-raised p-3 lg:border-b-0 lg:border-r">
<div class="mb-3 grid grid-cols-3 gap-2 text-center">
<div class="rounded-lg border border-border bg-background p-2">
<p class="text-lg font-semibold">8</p>
<p class="text-[11px] text-muted-fg">Endpoints</p>
</div>
<div class="rounded-lg border border-border bg-background p-2">
<p class="text-lg font-semibold">42k</p>
<p class="text-[11px] text-muted-fg">Events</p>
</div>
<div class="rounded-lg border border-border bg-background p-2">
<p class="text-lg font-semibold">99.1%</p>
<p class="text-[11px] text-muted-fg">Success</p>
</div>
</div>
<div class="space-y-2">
<button
v-for="endpoint in filteredEndpoints"
:key="endpoint.id"
type="button"
class="w-full rounded-lg border p-3 text-left transition hover:border-primary/50"
:class="endpoint.id === selectedEndpoint.id ? 'border-primary/60 bg-primary/5' : 'border-border bg-background'"
@click="selectEndpoint(endpoint)"
>
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
<p class="truncate text-sm font-semibold">{{ endpoint.name }}</p>
<p class="mt-1 truncate text-xs text-muted-fg">{{ endpoint.url }}</p>
</div>
<span class="rounded-full bg-secondary px-2 py-0.5 text-[11px] font-semibold text-muted-fg">
{{ endpoint.events.length }}
</span>
</div>
<div class="mt-3 flex items-center justify-between text-xs">
<span :class="endpoint.status === 'Healthy' ? 'text-emerald-700 dark:text-emerald-300' : 'text-warning'">
{{ endpoint.status }}
</span>
<span class="text-muted-fg">{{ endpoint.failures }} failures</span>
</div>
</button>
</div>
</aside>
<main class="space-y-4 p-4 sm:p-5">
<section class="grid gap-3 md:grid-cols-3">
<DomCard class="p-4">
<p class="text-xs font-medium text-muted-fg">Selected endpoint</p>
<p class="mt-2 truncate text-lg font-semibold">{{ selectedEndpoint.name }}</p>
<p class="mt-1 truncate text-xs text-muted-fg">{{ selectedEndpoint.url }}</p>
</DomCard>
<DomCard class="p-4">
<p class="text-xs font-medium text-muted-fg">Delivery health</p>
<p class="mt-2 text-lg font-semibold" :class="healthTone">{{ selectedEndpoint.successRate }}% success</p>
<p class="mt-1 text-xs text-muted-fg">{{ selectedEndpoint.failures }} failures in 24 hours</p>
</DomCard>
<DomCard class="p-4">
<p class="text-xs font-medium text-muted-fg">Median latency</p>
<p class="mt-2 text-lg font-semibold">{{ selectedEndpoint.latencyMs }}ms</p>
<p class="mt-1 text-xs text-muted-fg">Version {{ selectedEndpoint.apiVersion }}</p>
</DomCard>
</section>
<section class="rounded-xl border border-border bg-background">
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-border p-4">
<div>
<h4 class="font-semibold">Endpoint configuration</h4>
<p class="mt-1 text-sm text-muted-fg">Tune the destination, subscribed events, and delivery workflow.</p>
</div>
<div class="flex gap-2">
<DomButton variant="ghost" size="sm">Send test</DomButton>
<DomButton size="sm">Save changes</DomButton>
</div>
</div>
<div class="grid gap-3 border-b border-border p-4 md:grid-cols-[minmax(0,1fr)_11rem]">
<DomTextInput v-model="endpointUrl" label="Endpoint URL" type="url" />
<DomTextInput v-model="apiVersion" label="API version" />
</div>
<div class="border-b border-border px-4 pt-3">
<DomTabs v-model="activeTab" :tabs="tabs" />
</div>
<div v-if="activeTab === 'events'" class="grid gap-3 p-4 md:grid-cols-3">
<div v-for="group in eventGroups" :key="group.title" class="rounded-lg border border-border skin-raised p-3">
<h5 class="text-sm font-semibold">{{ group.title }}</h5>
<div class="mt-3 space-y-3">
<label v-for="event in group.events" :key="event.key" class="flex gap-3">
<input
type="checkbox"
class="mt-1 size-4 rounded border-border text-primary"
:checked="selectedEvents.includes(event.key)"
@change="toggleEvent(event.key)"
>
<span>
<span class="block text-sm font-medium">{{ event.label }}</span>
<span class="block font-mono text-[11px] text-muted-fg">{{ event.key }}</span>
<span class="block text-xs leading-5 text-muted-fg">{{ event.description }}</span>
</span>
</label>
</div>
</div>
</div>
<div v-else-if="activeTab === 'deliveries'" class="p-4">
<div class="mb-3 flex flex-wrap items-center justify-between gap-2">
<p class="text-sm text-muted-fg">Recent delivery attempts for {{ selectedEndpoint.name }}.</p>
<div class="flex gap-2">
<button
v-for="filter in ['All', 'Delivered', 'Retrying', 'Failed']"
:key="filter"
type="button"
class="rounded-full px-3 py-1 text-xs font-semibold transition"
:class="deliveryFilter === filter ? 'bg-primary text-primary-fg' : 'bg-secondary text-muted-fg hover:text-fg'"
@click="deliveryFilter = filter"
>
{{ filter }}
</button>
</div>
</div>
<div class="overflow-hidden rounded-lg border border-border">
<div
v-for="attempt in filteredAttempts"
:key="attempt.id"
class="grid gap-3 border-b border-border p-3 text-sm last:border-b-0 md:grid-cols-[minmax(0,1fr)_7rem_5rem_6rem_5rem]"
>
<div class="min-w-0">
<p class="truncate font-medium">{{ attempt.event }}</p>
<p class="font-mono text-xs text-muted-fg">{{ attempt.id }} / {{ attempt.time }}</p>
</div>
<span class="w-max rounded-full px-2 py-1 text-xs font-semibold" :class="deliveryClasses(attempt.status)">
{{ attempt.status }}
</span>
<p class="text-muted-fg">{{ attempt.responseCode }}</p>
<p class="text-muted-fg">{{ attempt.latencyMs }}ms</p>
<DomButton v-if="attempt.status !== 'Delivered'" variant="ghost" size="sm">Retry</DomButton>
</div>
</div>
</div>
<div v-else class="grid gap-3 p-4 md:grid-cols-[minmax(0,1fr)_16rem]">
<div class="rounded-lg border border-border skin-raised p-4">
<p class="text-sm font-semibold">Signing secret</p>
<div class="mt-3 flex flex-wrap items-center gap-2 rounded-lg border border-border bg-background p-3">
<code class="min-w-0 flex-1 truncate text-xs">{{ visibleSecret }}</code>
<DomButton variant="ghost" size="sm" @click="showSecret = !showSecret">
{{ showSecret ? 'Hide' : 'Reveal' }}
</DomButton>
</div>
<p class="mt-3 text-xs leading-5 text-muted-fg">Reveal secrets only to authorized admins and log every rotation.</p>
</div>
<div class="rounded-lg border border-border skin-raised p-4">
<DomToggle v-model="sendTestEvent" label="Send a test event after saving" />
<p class="mt-3 text-xs leading-5 text-muted-fg">Use a synthetic payload to confirm DNS, TLS, signature, and response handling.</p>
</div>
</div>
</section>
</main>
<aside class="space-y-3 border-t border-border skin-raised p-4 lg:border-l lg:border-t-0">
<section class="rounded-xl border border-border bg-background p-4">
<p class="text-sm font-semibold">Readiness</p>
<div class="mt-3 space-y-3">
<div class="flex items-center justify-between gap-3 text-sm">
<span class="text-muted-fg">Subscribed events</span>
<span class="font-semibold">{{ subscribedCount }}</span>
</div>
<div class="flex items-center justify-between gap-3 text-sm">
<span class="text-muted-fg">Secret rotation</span>
<span class="font-semibold">84 days</span>
</div>
<div class="flex items-center justify-between gap-3 text-sm">
<span class="text-muted-fg">Retry policy</span>
<span class="font-semibold">3 attempts</span>
</div>
</div>
</section>
<section class="rounded-xl border border-border bg-background p-4">
<p class="text-sm font-semibold">Launch checklist</p>
<div class="mt-3 space-y-2">
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" class="size-4 rounded border-border text-primary" checked>
<span>Endpoint returns 2xx</span>
</label>
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" class="size-4 rounded border-border text-primary" checked>
<span>Signature verified</span>
</label>
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" class="size-4 rounded border-border text-primary">
<span>Failure alert connected</span>
</label>
</div>
</section>
<section class="rounded-xl border border-border bg-background p-4">
<p class="text-sm font-semibold">Activity</p>
<div class="mt-3 space-y-3 text-sm">
<div>
<p class="font-medium">Delivery retried</p>
<p class="text-xs text-muted-fg">Ari Grant / 18 minutes ago</p>
</div>
<div>
<p class="font-medium">Events updated</p>
<p class="text-xs text-muted-fg">Maya Chen / Today 10:21</p>
</div>
<div>
<p class="font-medium">Secret rotated</p>
<p class="text-xs text-muted-fg">Security bot / Jun 01, 2026</p>
</div>
</div>
</section>
</aside>
</div>
</div>
</template>
Integration
How to use this block
Use this block when your product sends events to customer systems and developers need a place to configure, debug, and trust those deliveries. It combines endpoint settings with delivery observability so users can resolve failures without leaving your app.
- Replace
endpointswith workspace-scoped webhook endpoints from your backend. - Load event types from your event catalog so subscriptions stay aligned with server capabilities.
- Connect delivery rows to immutable delivery attempts and expose retry only for failed or timed-out attempts.
- Use the signing secret panel for one-time reveal, rotation confirmation, and copy-to-clipboard feedback.
- Persist test sends, retries, and subscription changes through API calls that also write audit events.
Data
Recommended webhook endpoint shape
{
id: 'we_prod_platform',
name: 'Production platform',
url: 'https://api.acme.example/webhooks/dom',
environment: 'Production',
status: 'Healthy',
apiVersion: '2026-06-01',
secretPrefix: 'whsec_4A7K',
events: ['customer.created', 'invoice.paid', 'subscription.updated'],
delivery: { successRate: 99.6, latencyMs: 318, failures: 2 },
attempts: [
{
id: 'evt_1092',
event: 'invoice.paid',
status: 'Delivered',
responseCode: 200,
latencyMs: 241,
time: '2 minutes ago'
}
]
}Customization
Implementation notes
Delivery model
Store every attempt separately from the logical event so retries keep a complete audit trail.
Security
Mask secrets after creation, sign payloads server-side, and show rotation state before accepting changes.
Future updates
Useful follow-ups include event schema docs, endpoint health alerts, retry policy editors, and webhook test fixtures.