> ## Documentation Index
> Fetch the complete documentation index at: https://docs.eigenpal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Receive run events with webhooks

> Send signed run lifecycle events from EigenPal to your application.

Outbound webhooks notify your application when an EigenPal run is created or
changes status. They are useful when a run takes too long to keep an API request
open and you do not want to poll the Runs API.

Webhooks are configured per organization in **Webhooks** in the EigenPal
dashboard. An endpoint can subscribe to either or both supported events:

* `run.created` — a top-level workflow or agent run was created.
* `run.status_changed` — a top-level run moved from one persisted status to
  another.

Runs created internally by an `action.invoke-workflow` step do not emit webhook
events.

## Create an endpoint

<Steps>
  <Step title="Open Webhooks">
    Select **Webhooks** in the dashboard sidebar and choose **Add webhook**.
  </Step>

  <Step title="Enter the destination">
    Add a descriptive name and an HTTPS URL that accepts `POST` requests.
  </Step>

  <Step title="Choose events">Select `run.created`, `run.status_changed`, or both.</Step>

  <Step title="Add optional headers">
    Add credentials such as an `Authorization` header when your receiver requires them. Header
    values are encrypted and are not shown again.
  </Step>

  <Step title="Store the signing secret">
    Copy the `whsec_...` signing secret from the creation dialog or the endpoint page. Use it to
    verify that webhook requests were sent by Eigenpal.
  </Step>
</Steps>

If you lose or suspect exposure of the signing secret, disable or delete the
endpoint and create a replacement. Secret rotation is not available yet.

## Event envelope

Every request has a versioned JSON envelope and a stable event ID:

```json theme={null}
{
  "id": "whev_01J...",
  "type": "run.status_changed",
  "apiVersion": "2026-07-01",
  "createdAt": "2026-07-13T12:00:00.000Z",
  "test": false,
  "data": {
    "run": {
      "id": "exec_01J...",
      "type": "workflow",
      "automationId": "aut_01J...",
      "status": "completed",
      "triggerType": "api"
    },
    "previousStatus": "running",
    "currentStatus": "completed"
  }
}
```

`run.created` contains the run in its initial status and omits
`previousStatus` and `currentStatus`.

When available, the public run output is included in the event. If the complete
event would exceed the webhook payload limit, `output` is omitted and
`outputOmitted` is `true`. Retrieve the canonical result using the included run
ID and [`GET /api/v1/runs/{id}`](/api-reference/runs/get-a-run).

## Verify signatures

Do not process an event until its signature and timestamp have been verified.
EigenPal signs the exact request bytes with HMAC-SHA256 and sends:

* `webhook-id`
* `webhook-timestamp`
* `webhook-signature`, formatted as `v1,<hex digest>`

The signed message is:

```text theme={null}
<webhook-id>.<webhook-timestamp>.<raw request body>
```

The following Node.js example verifies the signature without parsing or
reserializing the body:

```ts theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyEigenpalWebhook(rawBody: Buffer, headers: Headers, secret: string): boolean {
  const eventId = headers.get('webhook-id');
  const timestamp = headers.get('webhook-timestamp');
  const signature = headers.get('webhook-signature');
  if (!eventId || !timestamp || !signature?.startsWith('v1,')) return false;

  const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(ageSeconds) || ageSeconds > 300) return false;

  const expected = createHmac('sha256', secret)
    .update(`${eventId}.${timestamp}.`)
    .update(rawBody)
    .digest('hex');
  const received = signature.slice(3);

  if (!/^[a-f0-9]+$/i.test(received)) return false;
  const expectedBytes = Buffer.from(expected, 'hex');
  const receivedBytes = Buffer.from(received, 'hex');
  if (expectedBytes.length !== receivedBytes.length) return false;
  return timingSafeEqual(expectedBytes, receivedBytes);
}
```

Use the raw bytes supplied by your web framework. Parsing JSON and serializing it
again can change whitespace or key ordering and invalidate the signature.

## Delivery and retries

Webhook delivery is asynchronous and **at least once**. Your receiver must:

1. Verify the signature.
2. Store or deduplicate by the event `id`.
3. Return a `2xx` response quickly.
4. Process expensive work asynchronously.

EigenPal retries network failures, timeouts, `408`, `425`, `429`, and `5xx`
responses with bounded backoff. Other `4xx` responses end automatic delivery.
Any `2xx` response marks the delivery successful.

Events are not guaranteed to arrive in order. For status changes, use
`previousStatus`, `currentStatus`, and `createdAt` rather than assuming the
delivery sequence matches the run lifecycle.

The Webhooks dashboard shows each delivery and its attempts, including response
status and latency. Endpoints without custom headers also retain a bounded
response preview; EigenPal omits response bodies for credential-bearing endpoints
so echoed credentials cannot enter delivery history. You can manually redeliver
an event after fixing your receiver. Redelivery preserves the event ID and payload
so your normal deduplication logic still applies.

## Test an endpoint

Use **Send test** from an endpoint page. Test requests pass through the same
queue, signing, security checks, and delivery history as production events and
set `test` to `true`.

<Tip>
  Return `2xx` as soon as the event is durably accepted by your application. A slow response can
  time out even when your application eventually completes its work, causing a duplicate attempt.
</Tip>
