Webhooks
Helios sends a signed JSON request to your URL when a run finishes, a chat turn completes, a library change is staged, and more. Learn the request format, how to check the signature, and how payloads change over time.
A webhook target is a URL that Helios calls when an event you chose happens. You add targets in
Settings > Webhooks or with the helios webhook commands. Each target picks its events,
its scope (a project, the whole organization, or only you) and a payload version.
The request
Every delivery is an HTTPS POST with a JSON body of at most 64 KB. The body is the same for every
event type. Only type and data change.
{
"id": "wev_m1x8q2c7k4n9p0r3s5t6v8w1",
"type": "workflow.run.failed",
"version": "2026-10-01",
"occurredAt": "2026-10-12T09:41:27.512Z",
"organizationId": "org_4f7a1c2e9b",
"projectId": "prj_k2m8d1x5q7",
"data": {
"runId": "awr_h3j9t2v6b1",
"workflowId": "awf_p8c4n7w2z5",
"workflowName": "Nightly triage",
"status": "failed",
"error": { "code": "timeout" }
}
}| Field | Meaning |
|---|---|
id | The event ID. It is the same on every retry and resend, and equals the webhook-id header. |
type | The event type, such as workflow.run.failed. |
version | The payload version the target is pinned to. |
occurredAt | When the event happened, in ISO 8601 UTC. |
organizationId | Your organization ID. |
projectId | The project the event belongs to. It is absent for organization events such as project.created. |
data | The event's fields. See the event list for each type. |
Payloads are thin. They say what happened and point to the resource by ID, name and status. They
never carry content such as run output, message text, prompts, file contents, variable values or
secrets. Fetch the resource from the Helios API when you need more. Organization events carry project
IDs only, never project names. The path lists in library events and the member lists in
group.members_changed are cut short when they would make data larger than 60 KB, and then
carry truncated: true and the full count. Any other event whose data is still larger than
60 KB is not sent.
Reply with any 2xx status within 10 seconds to accept a delivery. Any other status, a redirect or a
timeout counts as a failure, and Helios retries it.
Check the signature
Helios signs every request with Standard Webhooks. Each request has three headers.
| Header | Value |
|---|---|
webhook-id | The event ID. |
webhook-timestamp | The send time in Unix seconds. It is new on every attempt. |
webhook-signature | v1, followed by a base64 HMAC-SHA256 of {id}.{timestamp}.{body}. |
The signing secret starts with whsec_. You see it when you create the target and when you rotate
it. Check the signature on the raw request body, before you parse it. The standard-webhooks
library does this for you.
npm install standard-webhooksimport { Webhook } from "standard-webhooks";
const webhook = new Webhook(process.env.HELIOS_WEBHOOK_SECRET ?? "");
export async function handleHeliosWebhook(request: Request): Promise<Response> {
const body = await request.text();
let event: { id: string; type: string; occurredAt: string; data: unknown };
try {
event = webhook.verify(body, Object.fromEntries(request.headers)) as typeof event;
} catch {
return new Response("Invalid signature", { status: 401 });
}
// Reply within 10 seconds. Do slow work in your own job queue.
await enqueueHeliosEvent(event);
return new Response(null, { status: 204 });
}The library rejects a request whose timestamp is more than 5 minutes away from your clock, which blocks replayed requests. Keep your server clock in sync.
When you rotate the secret, the old secret stays valid for 24 hours. During that time
webhook-signature holds two signatures separated by a space, one for each secret. The library
accepts the request if either one matches, so you can update your secret at any time in that window.
A second rotation inside that window replaces the overlap, so the oldest secret stops being used at
once.
Delivery and ordering
Delivery is at least once and unordered.
- Duplicates. A retry or a resend can deliver the same event again. Store the
webhook-idof the events you processed and skip an ID you have already seen. - Order. A retried
workflow.run.startedcan arrive afterworkflow.run.succeeded. UseoccurredAtto put events in order, not the time the request arrived. - Filtering. A target receives only the event types it subscribes to. There is no option to receive every event, and a new event type reaches no existing target until an admin adds it.
Versions
Each target is pinned to one dated payload version, such as 2026-10-01. New targets use the latest
version. An admin can change a target's version at any time. Helios builds each body when it sends
it, so the next delivery uses the new version, including a resend of an older event.
Within a version, payloads change only in ways that keep existing receivers working.
- A new event type.
- A new optional field.
- A new value of an existing enum field, such as a new
error.code.
To stay compatible, ignore fields you do not know and accept enum values you do not know.
Anything else ships only in a new version. This includes removing or renaming a field, changing a field's type or meaning, removing an event type, or changing when an event fires.
A version is supported for at least 12 months after the next version ships. From 90 days before
support ends, targets on that version show a banner with the end date and the version to move to.
When support ends, targets still on that version are turned off with the reason version_retired.
Last updated on
MCP servers
Connect any remote MCP server so Helios agents can call its tools, and expose Helios itself over MCP to your own agents.
Delivery and retries
The retry schedule, every failure reason, the throughput limits per target, when a delivery expires, when a failing target turns off, and what happens while a target or Helios is paused.