{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "webhooks-1",
  "title": "Webhooks 1: endpoints and deliveries",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A webhooks manager: endpoints with health from recent deliveries, signing secret and events, a delivery log with status codes and timing, payload and response per delivery with Send again, and an add dialog that validates.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/checkbox.json",
    "https://ui.ballmac.com/r/copy-button.json",
    "https://ui.ballmac.com/r/dialog.json",
    "https://ui.ballmac.com/r/input.json",
    "https://ui.ballmac.com/r/json-viewer.json",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json",
    "https://ui.ballmac.com/r/switch.json",
    "https://ui.ballmac.com/r/return-focus.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/webhooks-1/webhooks-1.tsx",
      "content": "// Ballmac UI: Webhooks 1. https://ui.ballmac.com/blocks/webhooks-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { CircleAlert, CircleCheck, CirclePause, Eye, EyeOff, Loader2, Plus, RotateCw, Webhook } from \"lucide-react\"\n\nimport { Button } from \"@/components/ballmac/button\"\nimport { Checkbox } from \"@/components/ballmac/checkbox\"\nimport { CopyButton } from \"@/components/ballmac/copy-button\"\nimport { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from \"@/components/ballmac/dialog\"\nimport { Input } from \"@/components/ballmac/input\"\nimport { JsonViewer } from \"@/components/ballmac/json-viewer\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { Switch } from \"@/components/ballmac/switch\"\nimport { useReturnFocus } from \"@/lib/ballmac/return-focus\"\nimport { cn } from \"@/lib/utils\"\n\ntype Webhooks1Delivery = {\n  id: string\n  event: string\n  /** HTTP status the endpoint answered with, or 0 when it did not answer. */\n  status: number\n  /** Milliseconds. */\n  ms: number\n  /** \"Today, 10:42\", already written for people. */\n  time: string\n  attempt: number\n  payload: unknown\n  response?: string\n}\n\ntype Webhooks1Endpoint = {\n  id: string\n  url: string\n  enabled: boolean\n  events: string[]\n  secret: string\n  deliveries: Webhooks1Delivery[]\n}\n\ntype Webhooks1Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    endpoints?: Webhooks1Endpoint[]\n    /** Every event an endpoint can subscribe to. */\n    availableEvents?: string[]\n    title?: string\n    onCreate?: (input: { url: string; events: string[] }) => void | Promise<void>\n    onToggle?: (endpoint: Webhooks1Endpoint, enabled: boolean) => void\n    /** Called to send a delivery again. Return a promise; reject to show the failure. */\n    onResend?: (endpoint: Webhooks1Endpoint, delivery: Webhooks1Delivery) => void | Promise<void>\n    defaultSelectedId?: string\n    /** The delivery shown open at first. */\n    defaultOpenDeliveryId?: string\n  }\n\nconst events = [\"order.created\", \"order.paid\", \"order.refunded\", \"customer.created\", \"invoice.overdue\", \"subscription.cancelled\"]\n\nconst ok = (status: number) => status >= 200 && status < 300\nconst mkDeliveries = (list: [string, number, number, string, number][]): Webhooks1Delivery[] =>\n  list.map(([event, status, ms, time, attempt], i) => ({\n    id: `d${i}${event}${status}`,\n    event,\n    status,\n    ms,\n    time,\n    attempt,\n    payload: { id: `evt_${1000 + i}`, type: event, created: 1760780000 + i * 600, data: { object: { id: `ord_${5200 + i}`, amount: 14400 + i * 250, currency: \"usd\", customer: \"cus_Nf3x\" } } },\n    response: status === 0 ? \"No response: the connection timed out after 10 s\" : ok(status) ? '{\"received\":true}' : status === 500 ? \"Internal Server Error\" : \"Not Found\",\n  }))\n\nconst sample: Webhooks1Endpoint[] = [\n  {\n    id: \"w1\", url: \"https://api.northwind.com/hooks/acme\", enabled: true, events: [\"order.created\", \"order.paid\", \"order.refunded\"], secret: \"whsec_8f3a1c2b9d4e7f60a5b1c3d2e4f6a7b8\",\n    deliveries: mkDeliveries([[\"order.paid\", 200, 142, \"Today, 10:41\", 1], [\"order.created\", 200, 118, \"Today, 10:38\", 1], [\"order.paid\", 500, 2310, \"Today, 10:02\", 3], [\"order.paid\", 500, 2188, \"Today, 09:58\", 2], [\"order.created\", 200, 131, \"Today, 09:40\", 1], [\"order.refunded\", 200, 156, \"Yesterday, 17:12\", 1]]),\n  },\n  {\n    id: \"w2\", url: \"https://hooks.slack-relay.io/T0482/B9921\", enabled: true, events: [\"customer.created\", \"subscription.cancelled\"], secret: \"whsec_c1a29b8d7e6f5a4b3c2d1e0f9a8b7c6d\",\n    deliveries: mkDeliveries([[\"customer.created\", 404, 88, \"Today, 10:20\", 5], [\"customer.created\", 404, 91, \"Today, 10:15\", 4], [\"subscription.cancelled\", 0, 10000, \"Today, 08:31\", 5], [\"customer.created\", 404, 85, \"Yesterday, 22:02\", 5]]),\n  },\n  {\n    id: \"w3\", url: \"https://staging.acme-test.dev/webhooks\", enabled: false, events: [\"invoice.overdue\"], secret: \"whsec_77aa11bb22cc33dd44ee55ff66aa77bb\",\n    deliveries: mkDeliveries([[\"invoice.overdue\", 200, 201, \"3 days ago\", 1]]),\n  },\n]\n\nconst health = (e: Webhooks1Endpoint) => {\n  if (!e.enabled) return { id: \"off\" as const, label: \"Disabled\", icon: CirclePause, rate: null }\n  const last = e.deliveries.slice(0, 10)\n  const rate = last.length ? Math.round((100 * last.filter((d) => ok(d.status)).length) / last.length) : 100\n  return rate >= 90 ? { id: \"ok\" as const, label: \"Healthy\", icon: CircleCheck, rate } : rate >= 50 ? { id: \"warn\" as const, label: \"Some failing\", icon: CircleAlert, rate } : { id: \"bad\" as const, label: \"Failing\", icon: CircleAlert, rate }\n}\nconst tone = { ok: \"text-chart-2\", warn: \"text-amber-600 dark:text-amber-400\", bad: \"text-destructive\", off: \"text-muted-foreground\" }\n\nfunction Skel() {\n  return (\n    <div aria-hidden=\"true\" className=\"grid gap-6 lg:grid-cols-[20rem_minmax(0,1fr)]\">\n      <div className=\"grid gap-3\">\n        {[0, 1, 2].map((i) => (\n          <Skeleton key={i} className=\"h-24 rounded-2xl\" />\n        ))}\n      </div>\n      <Skeleton className=\"h-[28rem] rounded-2xl\" />\n    </div>\n  )\n}\n\n/**\n * A webhooks manager: your endpoints with how healthy each is (worked out from its last ten deliveries), and for the chosen one its signing secret,\n * the events it listens to and a log of deliveries with status codes, timing and attempts. Open a delivery to see the payload and what the endpoint\n * answered, and send it again. Adding an endpoint checks the address is a secure URL and that at least one event is chosen.\n */\nfunction Webhooks1({\n  endpoints: endpointsProp = sample,\n  availableEvents = events,\n  title = \"Webhooks\",\n  onCreate,\n  onToggle,\n  onResend,\n  defaultSelectedId,\n  defaultOpenDeliveryId,\n  state = \"ready\",\n  onRetry,\n  emptyState,\n  errorState,\n  className,\n  ...props\n}: Webhooks1Props) {\n  const id = React.useId()\n  const [eps, setEps] = React.useState<Webhooks1Endpoint[]>(endpointsProp)\n  React.useEffect(() => setEps(endpointsProp), [endpointsProp])\n  const list = state === \"empty\" ? [] : eps\n  const [selectedId, setSelectedId] = React.useState(defaultSelectedId ?? list[0]?.id)\n  const [onlyFailed, setOnlyFailed] = React.useState(false)\n  const [openId, setOpenId] = React.useState<string | null>(defaultOpenDeliveryId ?? null)\n  const [reveal, setReveal] = React.useState(false)\n  const [resending, setResending] = React.useState<string | null>(null)\n  const [resendMsg, setResendMsg] = React.useState<{ id: string; ok: boolean; text: string } | null>(null)\n  const [adding, setAdding] = React.useState(false)\n  const [url, setUrl] = React.useState(\"\")\n  const [picked, setPicked] = React.useState<string[]>([])\n  const [formError, setFormError] = React.useState<string>()\n  const [saving, setSaving] = React.useState(false)\n  const ret = useReturnFocus(adding)\n  const sel = list.find((e) => e.id === selectedId) ?? list[0]\n  const h = sel ? health(sel) : null\n  const shown = sel ? sel.deliveries.filter((d) => !onlyFailed || !ok(d.status)) : []\n\n  async function create(e: React.FormEvent) {\n    e.preventDefault()\n    if (!/^https:\\/\\/[^\\s/]+\\.[^\\s/]+/.test(url.trim())) return setFormError(\"Enter a secure address that starts with https://, like https://example.com/hooks.\")\n    if (!picked.length) return setFormError(\"Choose at least one event to send.\")\n    setFormError(undefined)\n    setSaving(true)\n    try {\n      await onCreate?.({ url: url.trim(), events: picked })\n      const ep: Webhooks1Endpoint = { id: `w${Date.now()}`, url: url.trim(), enabled: true, events: picked, secret: `whsec_${Math.abs([...url].reduce((a, c) => (a * 31 + c.charCodeAt(0)) | 0, 7)).toString(16).padStart(32, \"a\")}`, deliveries: [] }\n      setEps((x) => [ep, ...x])\n      setSelectedId(ep.id)\n      setAdding(false)\n      setUrl(\"\")\n      setPicked([])\n    } catch (err) {\n      setFormError(err instanceof Error ? err.message : \"We could not add the endpoint. Nothing was changed.\")\n    } finally {\n      setSaving(false)\n    }\n  }\n  async function resend(d: Webhooks1Delivery) {\n    if (!sel) return\n    setResending(d.id)\n    setResendMsg(null)\n    try {\n      await onResend?.(sel, d)\n      setResendMsg({ id: d.id, ok: true, text: \"Sent again. Check the log for the new attempt.\" })\n    } catch (err) {\n      setResendMsg({ id: d.id, ok: false, text: err instanceof Error ? err.message : \"It could not be sent. Try again.\" })\n    } finally {\n      setResending(null)\n    }\n  }\n\n  const content = !list.length ? (\n    <div className=\"bg-card rounded-2xl border px-6 py-16 text-center\">\n      <span aria-hidden=\"true\" className=\"bg-muted mx-auto flex size-12 items-center justify-center rounded-full\">\n        <Webhook className=\"size-6\" />\n      </span>\n      <h2 className=\"mt-5 text-lg font-semibold tracking-tight\">{emptyState?.title ?? \"No endpoints yet\"}</h2>\n      <p className=\"text-muted-foreground mx-auto mt-1.5 max-w-sm text-sm text-pretty\">{emptyState?.description ?? \"Add an address and we will send it a signed message every time something happens.\"}</p>\n      {emptyState?.action ?? (\n        <Button className=\"mt-5\" onClick={() => setAdding(true)}>\n          <Plus aria-hidden=\"true\" />\n          Add an endpoint\n        </Button>\n      )}\n    </div>\n  ) : (\n    <div className=\"grid items-start gap-6 lg:grid-cols-[20rem_minmax(0,1fr)]\">\n      <ul aria-label=\"Endpoints\" className=\"grid gap-2.5\">\n        {list.map((e) => {\n          const hh = health(e)\n          const on = e.id === sel?.id\n          return (\n            <li key={e.id}>\n              <button type=\"button\" aria-pressed={on} onClick={() => { setSelectedId(e.id); setOpenId(null); setReveal(false) }} className={cn(\"bg-card focus-visible:ring-ring/50 hover:border-foreground/30 w-full rounded-2xl border p-4 text-start outline-none transition-colors focus-visible:ring-[3px] motion-reduce:transition-none\", on && \"border-foreground ring-foreground ring-1\")}>\n                <span className=\"block truncate font-mono text-sm font-medium\" dir=\"ltr\">\n                  {e.url.replace(/^https:\\/\\//, \"\")}\n                </span>\n                <span className=\"mt-2 flex items-center gap-2 text-sm\">\n                  <hh.icon aria-hidden=\"true\" className={cn(\"size-4\", tone[hh.id])} />\n                  <span className=\"font-medium\">{hh.label}</span>\n                  {hh.rate !== null ? <span className=\"text-muted-foreground tabular-nums\">· {hh.rate}% of the last {Math.min(10, e.deliveries.length) || 0}</span> : null}\n                </span>\n                <span className=\"text-muted-foreground mt-1 block text-xs\">{e.events.length} {e.events.length === 1 ? \"event\" : \"events\"}</span>\n              </button>\n            </li>\n          )\n        })}\n      </ul>\n\n      {sel && h ? (\n        <div className=\"min-w-0 grid grid-cols-1 gap-5\">\n          <div className=\"bg-card rounded-2xl border p-5 sm:p-6\">\n            <div className=\"flex flex-wrap items-start justify-between gap-4\">\n              <div className=\"min-w-0\">\n                <h2 className=\"truncate font-mono text-base font-semibold\" dir=\"ltr\">\n                  {sel.url}\n                </h2>\n                <p className=\"mt-1.5 flex items-center gap-2 text-sm\">\n                  <h.icon aria-hidden=\"true\" className={cn(\"size-4\", tone[h.id])} />\n                  {h.label}\n                </p>\n              </div>\n              <label className=\"flex items-center gap-2.5 text-sm font-medium\">\n                {sel.enabled ? \"Enabled\" : \"Disabled\"}\n                <Switch checked={sel.enabled} onCheckedChange={(v) => { setEps((x) => x.map((e) => (e.id === sel.id ? { ...e, enabled: v } : e))); onToggle?.(sel, v) }} aria-label={`Send events to ${sel.url}`} />\n              </label>\n            </div>\n            <div className=\"mt-5\">\n              <p id={`${id}-secret`} className=\"text-muted-foreground mb-1.5 text-xs font-medium\">\n                Signing secret\n              </p>\n              <div className=\"flex items-center gap-2 rounded-xl border p-1.5 ps-4\">\n                <p aria-labelledby={`${id}-secret`} className=\"min-w-0 flex-1 truncate font-mono text-sm\" dir=\"ltr\">\n                  {reveal ? sel.secret : `${sel.secret.slice(0, 6)}${\"•\".repeat(20)}`}\n                </p>\n                <Button variant=\"ghost\" size=\"icon\" aria-label={reveal ? \"Hide the secret\" : \"Show the secret\"} aria-pressed={reveal} onClick={() => setReveal(!reveal)}>\n                  {reveal ? <EyeOff aria-hidden=\"true\" /> : <Eye aria-hidden=\"true\" />}\n                </Button>\n                <CopyButton value={sel.secret} label=\"Copy\" copiedLabel=\"Copied\" variant=\"outline\" />\n              </div>\n            </div>\n            <ul aria-label=\"Events\" className=\"mt-5 flex flex-wrap gap-2\">\n              {sel.events.map((ev) => (\n                <li key={ev} className=\"bg-muted rounded-full px-3 py-1 font-mono text-xs\" dir=\"ltr\">\n                  {ev}\n                </li>\n              ))}\n            </ul>\n          </div>\n\n          <div className=\"bg-card min-w-0 rounded-2xl border\">\n            <div className=\"flex flex-wrap items-center justify-between gap-3 border-b px-5 py-3.5\">\n              <h3 className=\"font-semibold tracking-tight\">Recent deliveries</h3>\n              <label className=\"flex cursor-pointer items-center gap-2 text-sm\">\n                <input type=\"checkbox\" checked={onlyFailed} onChange={(e) => setOnlyFailed(e.target.checked)} className=\"accent-foreground size-4\" />\n                Failed only\n              </label>\n            </div>\n            {shown.length ? (\n              <ul className=\"divide-y\">\n                {shown.map((d) => {\n                  const good = ok(d.status)\n                  const open = openId === d.id\n                  return (\n                    <li key={d.id}>\n                      <button type=\"button\" aria-expanded={open} onClick={() => { setOpenId(open ? null : d.id); setResendMsg(null) }} className=\"hover:bg-accent/40 focus-visible:ring-ring/50 grid w-full grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-4 gap-y-1 px-5 py-3 text-start outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-inset motion-reduce:transition-none\">\n                        <span className={cn(\"inline-flex min-w-14 items-center justify-center gap-1 rounded-md border px-2 py-0.5 text-xs font-semibold tabular-nums\", good ? \"bg-chart-2/10 border-chart-2/30\" : \"bg-destructive/10 border-destructive/30\")}>\n                          {good ? <CircleCheck aria-hidden=\"true\" className=\"size-3\" /> : <CircleAlert aria-hidden=\"true\" className=\"size-3\" />}\n                          {d.status || \"No reply\"}\n                        </span>\n                        <span className=\"min-w-0\">\n                          <span className=\"block truncate font-mono text-sm\" dir=\"ltr\">\n                            {d.event}\n                          </span>\n                          <span className=\"text-muted-foreground block text-xs tabular-nums\">\n                            {d.time} · {d.ms >= 1000 ? `${(d.ms / 1000).toFixed(1)} s` : `${d.ms} ms`}\n                            {d.attempt > 1 ? ` · attempt ${d.attempt}` : \"\"}\n                          </span>\n                        </span>\n                        <span aria-hidden=\"true\" className=\"text-muted-foreground text-xs\">\n                          {open ? \"Hide\" : \"Details\"}\n                        </span>\n                      </button>\n                      {open ? (\n                        <div className=\"bg-muted/30 border-t px-5 py-4\">\n                          <div className=\"grid gap-4 lg:grid-cols-2\">\n                            <div className=\"min-w-0\">\n                              <p className=\"text-muted-foreground mb-2 text-xs font-medium\">Payload we sent</p>\n                              <JsonViewer value={d.payload} label={`Payload of ${d.event}`} defaultExpandedDepth={2} />\n                            </div>\n                            <div className=\"min-w-0\">\n                              <p className=\"text-muted-foreground mb-2 text-xs font-medium\">What your endpoint answered</p>\n                              <p className=\"bg-background rounded-lg border p-3 font-mono text-xs break-words\" dir=\"ltr\">\n                                {d.response ?? \"Nothing recorded\"}\n                              </p>\n                              <Button variant=\"outline\" size=\"sm\" className=\"mt-3\" disabled={resending === d.id} onClick={() => void resend(d)}>\n                                {resending === d.id ? <Loader2 aria-hidden=\"true\" className=\"animate-spin motion-reduce:animate-none\" /> : <RotateCw aria-hidden=\"true\" />}\n                                {resending === d.id ? \"Sending\" : \"Send again\"}\n                              </Button>\n                              {resendMsg?.id === d.id ? (\n                                <p role={resendMsg.ok ? \"status\" : \"alert\"} className=\"mt-3 text-sm\">\n                                  {resendMsg.text}\n                                </p>\n                              ) : null}\n                            </div>\n                          </div>\n                        </div>\n                      ) : null}\n                    </li>\n                  )\n                })}\n              </ul>\n            ) : (\n              <p className=\"text-muted-foreground px-5 py-10 text-center text-sm\">{onlyFailed ? \"No failed deliveries. Everything arrived.\" : \"Nothing has been sent to this endpoint yet.\"}</p>\n            )}\n          </div>\n        </div>\n      ) : null}\n    </div>\n  )\n\n  return (\n    <section data-slot=\"webhooks-1\" data-state={state} aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-6xl px-4 py-10 sm:px-6\", className)} {...frameProps(props)}>\n      <header className=\"mb-8 flex flex-wrap items-end justify-between gap-4\">\n        <div>\n          <h1 id={`${id}-title`} className=\"text-3xl font-semibold tracking-[-0.035em] text-balance\">\n            {title}\n          </h1>\n          <p className=\"text-muted-foreground mt-2 max-w-xl text-pretty\">Send a signed message to your own system whenever something happens here.</p>\n        </div>\n        {state === \"ready\" && list.length ? (\n          <Button onClick={() => setAdding(true)}>\n            <Plus aria-hidden=\"true\" />\n            Add endpoint\n          </Button>\n        ) : null}\n      </header>\n      <StateView state={state === \"loading\" || state === \"error\" ? state : \"ready\"} onRetry={onRetry} label=\"Loading webhooks\" loading={<Skel />} error={{ title: errorState?.title ?? \"Webhooks could not be loaded\", description: errorState?.description ?? \"Nothing was changed. Check your connection and try again.\", ...errorState }}>\n        {content}\n      </StateView>\n\n      <Dialog open={adding} onOpenChange={(o) => { if (!saving) { setAdding(o); if (!o) setFormError(undefined) } }}>\n        <DialogContent {...ret}>\n          <form onSubmit={create} noValidate>\n            <DialogHeader>\n              <DialogTitle>Add an endpoint</DialogTitle>\n              <DialogDescription>We will send a signed POST request to this address for each event you choose.</DialogDescription>\n            </DialogHeader>\n            <div className=\"mt-5 grid gap-5\">\n              <div>\n                <label htmlFor={`${id}-url`} className=\"mb-1.5 block text-sm font-medium\">\n                  Endpoint address\n                </label>\n                <Input id={`${id}-url`} dir=\"ltr\" type=\"url\" value={url} onChange={(e) => setUrl(e.target.value)} placeholder=\"https://example.com/hooks\" aria-invalid={!!formError && !/^https:\\/\\//.test(url)} autoComplete=\"off\" />\n              </div>\n              <fieldset>\n                <legend className=\"mb-2 text-sm font-medium\">Events to send</legend>\n                <div className=\"grid gap-2 sm:grid-cols-2\">\n                  {availableEvents.map((ev) => (\n                    <label key={ev} className=\"hover:bg-accent/40 flex cursor-pointer items-center gap-2.5 rounded-lg border p-2.5 text-sm\">\n                      <Checkbox checked={picked.includes(ev)} onCheckedChange={(v) => setPicked((p) => (v === true ? [...p, ev] : p.filter((x) => x !== ev)))} />\n                      <span className=\"font-mono text-xs\" dir=\"ltr\">\n                        {ev}\n                      </span>\n                    </label>\n                  ))}\n                </div>\n              </fieldset>\n              {formError ? (\n                <p role=\"alert\" className=\"text-destructive text-sm\">\n                  {formError}\n                </p>\n              ) : null}\n            </div>\n            <DialogFooter className=\"mt-6\">\n              <Button type=\"button\" variant=\"ghost\" onClick={() => setAdding(false)} disabled={saving}>\n                Cancel\n              </Button>\n              <Button type=\"submit\" disabled={saving}>\n                {saving ? <Loader2 aria-hidden=\"true\" className=\"animate-spin motion-reduce:animate-none\" /> : null}\n                {saving ? \"Adding\" : \"Add endpoint\"}\n              </Button>\n            </DialogFooter>\n          </form>\n        </DialogContent>\n      </Dialog>\n    </section>\n  )\n}\n\nexport { Webhooks1, type Webhooks1Props, type Webhooks1Endpoint, type Webhooks1Delivery }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/webhooks-1/webhooks-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "webhooks",
      "events",
      "endpoints",
      "deliveries",
      "developer",
      "integrations",
      "api",
      "retry",
      "signing secret",
      "logs"
    ],
    "version": "1.0.0",
    "updated": "2026-10-18",
    "tests": true,
    "ai": {
      "summary": "A webhooks manager. <Webhooks1 endpoints={[{ id, url, enabled, events: ['order.paid'], secret, deliveries: [{ id, event, status: 200 | 500 | 0 /* no reply */, ms, time, attempt, payload, response }] }]} availableEvents onCreate={({ url, events }) => Promise} onToggle onResend={(endpoint, delivery) => Promise} defaultSelectedId defaultOpenDeliveryId />. Health (Healthy 90% or more, Some failing 50% or more, Failing, Disabled) is worked out from each endpoint's last ten deliveries. Adding requires an https URL and at least one event. Reject onCreate or onResend to show the message in place.",
      "whenToUse": [
        "The developer settings of an API or SaaS product",
        "Debugging why events did not arrive"
      ],
      "whenNotToUse": [
        "A single webhook status tile (use webhook-card)",
        "Raw logs of everything (use log-stream)"
      ],
      "composesWith": [
        "dev-1",
        "settings-4",
        "settings-5",
        "app-shell-1"
      ],
      "a11y": [
        {
          "keys": "Tab, Enter / Space",
          "action": "Each endpoint is one button (pressed when chosen); each delivery row is a button that says whether it is expanded"
        },
        {
          "keys": "Status",
          "action": "Health and every delivery status are written (Healthy, Failing, 500, No reply) beside an icon, never by color alone"
        },
        {
          "keys": "Secret",
          "action": "Hidden by default with a Show button that says its state; Copy confirms in text. The add dialog returns focus to its button"
        }
      ],
      "customization": [
        "Addresses, secrets and event names are shown left to right in right-to-left layouts, like any code",
        "Health looks at the last ten deliveries, newest first",
        "Send again reports success or failure under the delivery that was resent"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "webhooks-1-loading",
        "trigger": "state=\"loading\"",
        "note": "A skeleton list and panel under the real heading, announced as loading."
      },
      {
        "id": "empty",
        "title": "No endpoints",
        "example": "webhooks-1-empty",
        "trigger": "state=\"empty\" (or endpoints={[]})",
        "note": "Explains what an endpoint is and offers Add an endpoint; emptyState sets the copy."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "webhooks-1-error",
        "trigger": "state=\"error\"",
        "note": "An alert saying nothing was changed, with Try again (onRetry). A failed Send again and a failed add show their message in place."
      }
    ],
    "examples": [
      "webhooks-1-demo",
      "webhooks-1-failing",
      "webhooks-1-loading",
      "webhooks-1-empty",
      "webhooks-1-error"
    ],
    "url": "https://ui.ballmac.com/components/webhooks-1"
  },
  "categories": [
    "blocks",
    "webhooks"
  ],
  "type": "registry:block"
}