Skip to content
Tier

Webhooks

Pro

The endpoints the app sends events to: each one's address, the events it gets, whether it's on, and its recent deliveries with their outcome, HTTP status and time taken. A test send shows its result where it was pressed. Adding an endpoint asks for an https address and its events (none is ticked for you). Removing asks first and says what stops; deliveries already made stay made. Keep it returns focus to the endpoint's Remove button; after a remove it goes to the Webhooks heading, since the endpoint's row goes. An endpoint is shown and named by its label when it has one, so a URL with a token in it (Slack, Discord) stays off the screen and out of screen readers.

Markdown
Version 5.2.0· other versions

This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).

Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1 and 2.6.2: the page came later, or had another name then.

Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.

Preview

Loading the preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/webhooks

Brings in: alert-dialog, bidi, button, checkbox, data, heading-level, heading-scope, input, locale, locked, page-state, relative-time, switch, tokens, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

import * as React from "react"
import { Webhooks, type Webhook } from "@/components/candor/webhooks"

const ago = (m: number) => new Date(Date.now() - m * 60_000).toISOString()
const events = [
  { id: "run.finished", label: "Run finished" },
  { id: "run.failed", label: "Run failed" },
  { id: "approval.requested", label: "Approval requested", description: "An agent is waiting on a person" },
]

export default function Example() {
  const [hooks, setHooks] = React.useState<Webhook[]>([
    {
      id: "w1",
      url: "https://ops.example.com/hooks/agents",
      events: ["run.failed", "approval.requested"],
      enabled: true,
      deliveries: [
        { id: "d2", event: "run.failed", at: ago(12), outcome: "delivered", status: 200, durationMs: 142 },
        { id: "d1", event: "approval.requested", at: ago(95), outcome: "failed", status: 503, durationMs: 10_000, error: "Timed out after 10 s" },
      ],
    },
  ])
  return (
    <Webhooks
      className="max-w-xl"
      webhooks={hooks}
      events={events}
      onAdd={(url, chosen) => setHooks((h) => [...h, { id: url, url, events: chosen, enabled: true, deliveries: [] }])}
      onToggle={(id, enabled) => setHooks((h) => h.map((x) => (x.id === id ? { ...x, enabled } : x)))}
      onTest={async () => {
        await new Promise((r) => setTimeout(r, 500))
        return { id: "test", event: "run.finished", at: new Date().toISOString(), outcome: "delivered", status: 200, durationMs: 98 }
      }}
      onRemove={(id) => setHooks((h) => h.filter((x) => x.id !== id))}
    />
  )
}

<Webhooks>

PropTypeDefaultDescription
webhooksrequiredWebhook[]—The endpoints.
eventsrequiredWebhookEvent[]—Every event an endpoint can subscribe to.
onAdd((url: string, events: string[]) => void | Promise<void>) | undefined—Shows Add endpoint. Called with the URL and the event ids ticked; the form waits on a returned promise.
onToggle((id: string, enabled: boolean) => void) | undefined—Shows a switch on each endpoint; called with its id and whether it should now be on.
onTest((id: string) => Promise<WebhookDelivery>) | undefined—Shows Send test on each endpoint. Resolve with the delivery the app recorded, and its result shows under the endpoint. If it throws or rejects, the endpoint says the test couldn't be sent, with the error's message as the reason.
onRemove((id: string) => void | Promise<void>) | undefined—Shows Remove on each endpoint. Called with its id once the person confirms; the dialog waits on a returned promise. If it throws or rejects, the dialog stays open and says it failed.
errorstring | undefined—Why the last add failed, in the app's words, shown in the form.
stateListState | undefined—What the list is showing: its rows (ready, the default), loading, empty or error with the reason, or partial with a note over the rows. Loading, empty and error draw in place of the rows, in this component's own words, so a list the app doesn't have yet isn't shown as an empty one.
onRetry(() => void) | undefined—Shows Try again when `state` is an error; called when it's pressed. Pass it only when it loads the list again.
classNamestring | undefined—Extra classes for the outer element.
headingLevelHeadingLevel | undefined—The level of the list's heading, so it fits under the page's own headings. Default: one below the nearest `HeadingScope`, else 2 (`<h2>`).

Strings

The fixed words, in English by default. Change any of them under webhooks in <LocaleProvider strings={{ … }}>; the table is exported as webhooksStrings.

KeyEnglish
labelWebhooks
headingWebhooks
addAdd endpoint
emptyNo endpoints yet. Nothing is sent anywhere.
loadingLoading endpoints
errorEndpoints couldn’t be loaded.
urlEndpoint URL
urlPlaceholderhttps://example.com/hooks/agents
eventsEvents to send
addItAdd
addingAdding…
lockedAddingAdding the endpoint
lockedTestingSending the test
cancelCancel
needsUrlEnter the endpoint’s address.
needsHttpsUse an https:// address, so events are encrypted on the way.
needsEventChoose at least one event.
sendingSends {events}
pausedPaused: nothing is sent to it
sendToSend events to {url}
testSend test
testingSending…
testDeliveredTest delivered
testFailedTest failed
testPendingTest sent, no answer yet
testNotRunCouldn’t send the test
testNotRunWhyNo delivery was recorded, so whether the endpoint answers isn’t known.
deliveriesRecent deliveries
noDeliveriesNothing delivered yet.
eventEvent
whenWhen
resultResult
deliveredDelivered
failedFailed
pendingPending
httpStatusHTTP {code}
took{ms} ms
removeRemove
removeEndpointRemove {url}
removeTitleRemove this endpoint?
removeStops{url} stops receiving events from the next one on. Deliveries already made aren’t recalled.
removeIrreversibleRemoving can’t be undone. To send to it again, add it again.
keepKeep it
addFailedAdding the endpoint failed.
removeFailedRemoving failed. Try again, or keep the endpoint.
keepLockedRemoving is under way, so the endpoint can’t be kept now. If it fails, you can keep it.

Types

WebhookDelivery
One attempt to deliver an event to an endpoint, as the app recorded it.
{ id: string; /** The event's id from `events`. */ event: string; at: string; outcome: "delivered" | "failed" | "pending"; /** The HTTP status the endpoint answered with, when it answered. */ status?: number | undefined; /** How long the endpoint took to answer, in ms, as measured by the app. */ durationMs?: number | undefined; /** Why it failed, in the app's words ("Timed out after 10 s"). */ error?: string | undefined; }
Webhook
An endpoint the app sends events to.
{ id: string; url: string; /** * How the endpoint reads on screen, in its labels and in the remove dialog, in place of `url`: "Slack #deploys", * "hooks.slack.com/…/B07". Set it when the URL carries a secret, as Slack and Discord webhook URLs do, so the token * isn't shown or read aloud. Left out, the URL is. */ label?: string | undefined; /** Event ids from `events`. */ events: string[]; /** Whether events are sent to it now. */ enabled: boolean; /** The latest deliveries, newest first. Leave it out when the app doesn't keep them. */ deliveries?: WebhookDelivery[] | undefined; }
WebhookEvent
An event an endpoint can subscribe to.
{ id: string; /** What happened, e.g. "Run finished". */ label: string; description?: string | undefined; }

Types from other items

The props above take these. Each is defined by another item; import it from the path shown.

ListState from @/lib/candor/data
What a list or table is showing, the same shape as `ChartState` and as page-state's `ListState` (which the free tier ships, so it declares its own rather than import this file): its rows (ready), the shape of rows still coming (loading), nothing and why (empty), the rows it has with a note on what's missing (partial), or why it couldn't load (error). Lists without a `state` show their rows.
ChartState
HeadingLevel from @/lib/candor/heading-level
A heading's level in the page's outline: `1` is `<h1>`, `6` is `<h6>`. Every Candor item that draws a heading takes its `headingLevel` as this type; the level is where the heading sits in the outline, never how large it looks.
1 | 2 | 3 | 4 | 5 | 6
ChartState from @/lib/candor/data
{ kind: "ready"; } | { kind: "loading"; } /** Nothing to plot, and why. */ | { kind: "empty"; reason: string; } /** Some of the range is missing. */ | { kind: "partial"; note: string; } | { kind: "error"; reason: string; }