> ## 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.

# Webhook receiver reference

> Receive, verify, and process EigenPal webhook events.

EigenPal sends webhook events as HTTPS `POST` requests with a JSON body. This
page defines the receiver contract, event schemas, signature verification, and
delivery behavior.

For dashboard setup and testing, see
[Receive run events with webhooks](/guides/outbound-webhooks).

## Request contract

Your receiver must:

1. Accept HTTPS `POST` requests.
2. Read and retain the exact request body bytes.
3. Verify the timestamp and HMAC signature before parsing or processing the event.
4. Deduplicate events by their stable `id`.
5. Durably accept the event and return a `2xx` response quickly.
6. Process expensive work asynchronously.

Requests use `Content-Type: application/json` and include these headers:

<ResponseField name="webhook-id" type="string" required>
  The stable event ID. It matches the `id` field in the body and is included in
  the signed message.
</ResponseField>

<ResponseField name="webhook-timestamp" type="string" required>
  The Unix timestamp, in seconds, when the delivery attempt was signed.
</ResponseField>

<ResponseField name="webhook-signature" type="string" required>
  The HMAC-SHA256 signature, formatted as `v1,&lt;hex digest&gt;`.
</ResponseField>

<ResponseField name="user-agent" type="string" required>
  Identifies the EigenPal webhook sender and release.
</ResponseField>

Any custom headers configured for the endpoint are included as well.

## Event envelope

Every request has a versioned JSON envelope:

```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...",
      "automationId": "aut_01J...",
      "type": "workflow",
      "status": "completed",
      "triggerType": "api",
      "createdAt": "2026-07-13T11:59:30.000Z",
      "startedAt": "2026-07-13T11:59:31.000Z",
      "completedAt": "2026-07-13T12:00:00.000Z",
      "output": {
        "invoiceNumber": "INV-1042"
      }
    },
    "previousStatus": "running",
    "currentStatus": "completed"
  }
}
```

### Envelope fields

<ResponseField name="id" type="string" required>
  Stable event ID with a `whev_` prefix. Use this value as the idempotency key.
</ResponseField>

<ResponseField name="type" type="run.created | run.status_changed" required>
  Identifies the event schema.
</ResponseField>

<ResponseField name="apiVersion" type="string" required>
  Version of the webhook wire contract. The current version is `2026-07-01`.
</ResponseField>

<ResponseField name="createdAt" type="string" required>
  ISO 8601 timestamp for when the event was created.
</ResponseField>

<ResponseField name="test" type="boolean" required>
  `true` for events created with **Send test**; otherwise `false`.
</ResponseField>

When `test` is `true`, `data.run` contains synthetic sample data. Its run and
automation IDs do not identify persisted resources. Verify and acknowledge the
request normally, but do not fetch or process the sample as a real run.

<ResponseField name="data" type="object" required>
  Event-specific payload.
</ResponseField>

### Run fields

<ResponseField name="data.run.id" type="string" required>
  Run ID.
</ResponseField>

<ResponseField name="data.run.automationId" type="string" required>
  Automation that owns the run.
</ResponseField>

<ResponseField name="data.run.type" type="workflow | agent" required>
  Runtime used by the automation.
</ResponseField>

<ResponseField name="data.run.status" type="string" required>
  Current persisted run status.
</ResponseField>

<ResponseField name="data.run.triggerType" type="string" required>
  How the run was started, such as `api`, `manual`, `email`, or `cron`.
</ResponseField>

<ResponseField name="data.run.createdAt" type="string" required>
  ISO 8601 timestamp for when the run was created.
</ResponseField>

<ResponseField name="data.run.startedAt" type="string | null" required>
  ISO 8601 timestamp for when execution started, or `null`.
</ResponseField>

<ResponseField name="data.run.completedAt" type="string | null" required>
  ISO 8601 timestamp for when the run reached a terminal status, or `null`.
</ResponseField>

<ResponseField name="data.run.output" type="unknown">
  Public run output, when available and small enough to include.
</ResponseField>

<ResponseField name="data.run.outputOmitted" type="boolean">
  `true` when `output` was removed to keep the complete event within the payload
  limit. Retrieve the canonical result with
  [`GET /api/v1/runs/{id}`](/api-reference/runs/get-a-run).
</ResponseField>

## Event types

### `run.created`

Sent when a top-level workflow or agent run is durably created. `data.run`
contains the initial run snapshot. Most workflow runs are already `pending` when
this event is sent; agent runs can initially be `created` while preparation
completes.

This event does not include `previousStatus` or `currentStatus`. Test deliveries
use the same schema with synthetic run data.

### `run.status_changed`

Sent whenever a top-level run moves from one persisted status to another.
Test deliveries simulate this transition with synthetic run data.

<ResponseField name="data.previousStatus" type="string" required>
  Status the run moved from.
</ResponseField>

<ResponseField name="data.currentStatus" type="string" required>
  Status the run moved to. This matches `data.run.status`.
</ResponseField>

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

### Run statuses

* `created` — The run exists and is being prepared before it enters the queue.
* `pending` — The run is queued and eligible for a worker.
* `running` — A worker is actively executing the run.
* `waiting` — The workflow is paused for a human, tool, or step continuation.
  Not all run types use this status.
* `finalizing` — Primary execution has finished, but outputs or other post-run
  work are still being saved.
* `completed` — The run finished successfully. Terminal.
* `failed` — The run ended because of an error or timeout. Terminal.
* `cancelled` — The run was cancelled before completion. Terminal.
* `rejected` — The run was refused before it started, for example because of an
  inbound email policy. Terminal.

`queued` is an execution phase, not a run status. The externally visible status
for a run waiting in the queue is `pending`.

## Verify webhook signatures

Do not process an event until its signature and timestamp have been verified.
EigenPal signs this message with the endpoint's `whsec_...` secret:

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

The signature is an HMAC and is **verified**, not decrypted. The following
Node.js example accepts a five-minute timestamp tolerance and compares the
digests in constant time:

```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 framework. Parsing JSON and serializing it
again can change whitespace or key ordering and invalidate the signature. Parse
the body only after verification succeeds.

Reject missing, malformed, or stale signature headers. Store the signing secret
outside source control. If the secret is lost or exposed, disable or delete the
endpoint and create a replacement.

## Delivery and retries

Delivery is asynchronous and **at least once**. Duplicate events are expected,
and events are not guaranteed to arrive in order.

* Deduplicate by the event `id`.
* Use `previousStatus`, `currentStatus`, and `createdAt` instead of inferring
  lifecycle order from delivery order.
* Return `2xx` as soon as the event is durably accepted.
* Process expensive work after acknowledging the request.

Any `2xx` response marks a delivery successful. EigenPal retries network
failures, timeouts, `408`, `425`, `429`, and `5xx` responses with bounded
backoff. Other `4xx` responses end automatic delivery. A valid `Retry-After`
response header can delay the next retry within EigenPal's configured limit.

Manual redelivery preserves the event ID and payload, so normal deduplication
still applies.
