Connect Agent run events to your backend, workflows, and monitoring tools with signed webhooks.
Webhooks send signed JSON events to your server as Agent runs start, complete, fail, or are cancelled. Use them to update your application, trigger workflows, collect analytics, or notify your team. Choose the events your integration needs.
HEROUI_WEBHOOK_SECRET. Set HEROUI_AGENT_ID to your Agent's ID.Select at least one event when the endpoint is enabled. Workspace owners manage endpoints and rotate secrets; workspace members can view delivery status. The secret is shown only on creation or rotation and cannot be read through client queries.
Choose individual events or use Select all to select all currently available events:
| Event | When it is emitted | Run status |
|---|---|---|
run.started | A new run is accepted for processing. | streaming |
run.completed | A run finishes successfully. | completed |
run.failed | A run ends with a terminal error. Recovered tool errors are excluded. | failed |
run.cancelled | A run is stopped before completion. It does not also emit run.failed. | aborted |
Existing endpoints without a saved event selection continue receiving only run.failed. Selecting all events does not automatically subscribe to event types added in future releases. Test deliveries always use webhook.test and do not create a run.
Every run event uses the same envelope. For example, a completed run sends:
{
"api_version": 1,
"id": "56624ad8-091d-4fbe-832e-c69bb90ab062",
"type": "run.completed",
"created_at": "2026-09-16T12:00:00.000Z",
"agent_id": "0aa18392-a4ae-49b4-b505-9512c7cdc6ae",
"data": {
"id": "56624ad8-091d-4fbe-832e-c69bb90ab062",
"object": "run",
"status": "completed",
"error_code": null,
"metadata": {"server": "east"}
}
}The example shortens data for readability. Run events contain a snapshot of the full public Run representation, including metadata and available timing. Start snapshots do not yet contain completion results. Each event's id is stable across retries, and created_at is its occurrence time. The start and terminal events for a run have different event IDs; use data.id to correlate the run. The X-HeroUI-Event-Id header repeats the event ID.
Read the raw request body before parsing JSON. X-HeroUI-Signature has the form t=unix_seconds,v1=hex_digest. The digest is HMAC-SHA256 over timestamp + "." + rawBody using the signing secret. Each retry receives a fresh timestamp and signature. Keep your server clock synchronized and reject timestamps outside a five-minute tolerance.
import {createHmac, timingSafeEqual} from "node:crypto";
export function verifyHeroUISignature(raw: string, header: string, secret: string) {
const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header);
if (!match) return false;
const [, timestamp, digest] = match;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${timestamp}.${raw}`).digest();
return timingSafeEqual(expected, Buffer.from(digest!, "hex"));
}Verify both the signature and expected agent_id. Never use event metadata as authorization. Store the secret in server environment variables and do not send it to the browser.
This receiver uses standard Request and Response APIs with Node.js signature verification. Save the helper above as verify-heroui-signature.ts.
The other two imports are application code you supply: processOnce handles durable deduplication, and handleRunEvent routes each event to your application or provider. For work that may exceed the 10-second delivery timeout, have the handler persist a job to your existing queue before acknowledging the event.
import {verifyHeroUISignature} from "./verify-heroui-signature";
import {processOnce} from "./webhook-event-store";
import {handleRunEvent} from "./run-event-handler";
export async function POST(request: Request) {
const raw = await request.text();
if (
!verifyHeroUISignature(
raw,
request.headers.get("X-HeroUI-Signature") ?? "",
process.env.HEROUI_WEBHOOK_SECRET!,
)
)
return new Response("Invalid signature", {status: 401});
const event = JSON.parse(raw);
if (event.agent_id !== process.env.HEROUI_AGENT_ID) {
return new Response("Unexpected agent", {status: 403});
}
if (event.api_version !== 1) return new Response("Unsupported version", {status: 400});
if (event.type === "webhook.test") return new Response(null, {status: 204});
switch (event.type) {
case "run.started":
case "run.completed":
case "run.failed":
case "run.cancelled":
await processOnce(event.id, () => handleRunEvent(event));
break;
}
return new Response(null, {status: 204});
}processOnce must claim a unique event ID, execute the callback, mark success only after the callback succeeds, and permit retry after failure. Concurrent claims must wait or return a retryable response. Let processing failures return a 5xx response so HeroUI can retry. Make downstream side effects idempotent too: a process can stop after an external action succeeds but before its completion is recorded.
HeroUI webhooks use HTTPS and JSON, so you can integrate them with your own backend or a service that accepts custom webhook payloads. Verify the signature before triggering actions. If a destination expects its own payload format or authentication, use your receiver to translate the event and call that provider's API.
| Destination | Example integration |
|---|---|
| Your application | Update activity when run.started arrives, store results on run.completed, and record failures or cancellations. |
| Workflow automation | Route events into an n8n Webhook node or a Pipedream HTTP trigger. Verify signatures in the workflow or in a receiver in front of it. |
| Analytics and monitoring | Map run events, metadata, and timing into your provider's event format. |
| Team notifications | Send selected events to your chat or email provider through its API. |
To forward run.failed events to Sentry, add this branch to your application's handleRunEvent after initializing the Sentry Node SDK. Other event types can continue to your other integrations.
import * as Sentry from "@sentry/node";
// Inside handleRunEvent(event), after signature verification and deduplication:
if (event.type === "run.failed") {
const errorCode = event.data.error_code ?? "unknown";
Sentry.captureEvent({
event_id: event.id.replaceAll("-", ""),
level: "error",
message: `HeroUI Agent run failed: ${errorCode}`,
fingerprint: ["heroui-agent-run-failed", event.agent_id, errorCode],
tags: {"heroui.agent_id": event.agent_id, "heroui.error_code": errorCode},
contexts: {"heroui.run": event.data},
});
if (!(await Sentry.flush(2000))) throw new Error("Sentry delivery timed out");
}This groups failures by agent and error code and attaches the run details. Review the metadata you forward using your provider's data-handling settings. See Sentry's event fingerprinting guide for grouping options.
Recent deliveries shows the event type, status, attempt count, and a link to the run. A failed delivery means your endpoint could not receive an event; this is separate from the run's status.
The receiver has 10 seconds to return a 2xx response. Failure or timeout schedules up to three retries after approximately 10, 60, and 300 seconds. Queue delivery can add delay. Redirects are not followed. After all four HTTP attempts fail, the delivery is marked Failed.
Five consecutive exhausted delivery cycles automatically pause the endpoint. A successful delivery resets the streak, including a success after an automatic retry. Test deliveries follow the same rules. Skipped events, internal queue failures, and interrupted requests with unknown outcomes do not increment the streak.
The dashboard shows Paused, the reason, and a Resume action for workspace owners. While paused, new run events are not scheduled and pending deliveries are skipped. Fix your endpoint, save any settings changes, then choose Resume. This enables the endpoint and resets its failure streak without changing its signing secret. Editing settings or rotating the secret alone does not resume delivery.
Resume applies to future events. It does not replay events from the pause or restart pending deliveries from before the pause. You can retry failed deliveries individually.
In Recent deliveries, choose Retry beside a failed event. The endpoint must be enabled, unpaused, and subscribed to that event type. Save or discard any settings changes first. Workspace members can inspect delivery history; owners can retry.
A manual retry starts a fresh cycle of up to four HTTP attempts at the currently configured endpoint, using its current signing secret. It preserves the original event ID, occurrence time, and payload—including the run snapshot and metadata. Continue deduplicating by event ID: a previous attempt may have reached your server even if its response was lost. Repeated requests to retry the same failed cycle do not create additional cycles.
Expand a delivery to see its Attempt history, including timestamps, available durations, HTTP status codes, and outcomes across the initial delivery and manual retries. Use View run to open the associated run. The total attempt count is retained. Older deliveries keep their existing counts; details for attempts made before history recording was available cannot be reconstructed. Send test creates a new webhook.test event instead of retrying an existing event.
Internal queue recovery is limited to 24 hours from the start of each delivery cycle and at most three dead-letter requeues that can resume HTTP delivery. Once a limit is reached, the primary consumer marks the delivery Failed without another request to your endpoint. A manual retry starts a new recovery window. Queue recovery failures do not count toward automatic endpoint pausing.
Delivery is at least once: acknowledge duplicate event IDs without repeating your side effect. Use a durable unique-key store in your receiver; keep IDs for at least your operational replay window. Do not rely on an in-memory set in a serverless process.
Delivery runs independently from conversation processing. Events may arrive out of order, so use their occurrence time and event type rather than arrival order. Updating subscriptions affects newly projected events and skips pending deliveries for events you deselect; it does not replay historical runs. Disabling, changing an endpoint URL, or rotating its secret causes deliveries tied to obsolete endpoint versions to be skipped. Configure and test the new receiver before switching production traffic.