Webhooks
ProThe 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.
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
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
"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>
| Prop | Type | Default | Description |
|---|---|---|---|
| webhooksrequired | Webhook[] | — | The endpoints. |
| eventsrequired | WebhookEvent[] | — | 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. |
| error | string | undefined | — | Why the last add failed, in the app's words, shown in the form. |
| state | ListState | 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. |
| className | string | undefined | — | Extra classes for the outer element. |
| headingLevel | HeadingLevel | 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.
| Key | English |
|---|---|
| label | Webhooks |
| heading | Webhooks |
| add | Add endpoint |
| empty | No endpoints yet. Nothing is sent anywhere. |
| loading | Loading endpoints |
| error | Endpoints couldn’t be loaded. |
| url | Endpoint URL |
| urlPlaceholder | https://example.com/hooks/agents |
| events | Events to send |
| addIt | Add |
| adding | Adding… |
| lockedAdding | Adding the endpoint |
| lockedTesting | Sending the test |
| cancel | Cancel |
| needsUrl | Enter the endpoint’s address. |
| needsHttps | Use an https:// address, so events are encrypted on the way. |
| needsEvent | Choose at least one event. |
| sending | Sends {events} |
| paused | Paused: nothing is sent to it |
| sendTo | Send events to {url} |
| test | Send test |
| testing | Sending… |
| testDelivered | Test delivered |
| testFailed | Test failed |
| testPending | Test sent, no answer yet |
| testNotRun | Couldn’t send the test |
| testNotRunWhy | No delivery was recorded, so whether the endpoint answers isn’t known. |
| deliveries | Recent deliveries |
| noDeliveries | Nothing delivered yet. |
| event | Event |
| when | When |
| result | Result |
| delivered | Delivered |
| failed | Failed |
| pending | Pending |
| httpStatus | HTTP {code} |
| took | {ms} ms |
| remove | Remove |
| removeEndpoint | Remove {url} |
| removeTitle | Remove this endpoint? |
| removeStops | {url} stops receiving events from the next one on. Deliveries already made aren’t recalled. |
| removeIrreversible | Removing can’t be undone. To send to it again, add it again. |
| keep | Keep it |
| addFailed | Adding the endpoint failed. |
| removeFailed | Removing failed. Try again, or keep the endpoint. |
| keepLocked | Removing 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; }