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

# Webhooks

> Get HTTP callbacks when tasks change state.

Webhooks deliver events to an HTTPS endpoint you control. Use them to
react to task completion or failure without polling.

## Before you start

* The **Webhooks** entry needs to be in your sidebar. If it isn't,
  webhooks aren't enabled on this deployment — contact your team
  admin.
* You need an admin role in the team that will own the webhook.
* An HTTPS endpoint reachable from the public internet. `http://`
  is not accepted.

## Create a webhook

1. Open **Webhooks** in the sidebar.
2. Click **Create webhook**.
3. Enter:
   * **URL** — your endpoint. Must be `https://`.
   * **Events** — pick which events trigger delivery. See the list
     below.
   * **Description** — internal label.
4. Click **Create**. The dashboard shows the webhook's **signing
   secret**. Copy it now — like credentials, it's shown once.

## Events

Two task events are delivered today.

| Event            | When it fires                                                                   |
| ---------------- | ------------------------------------------------------------------------------- |
| `task.completed` | Task finished cleanly. Payload includes the task output.                        |
| `task.failed`    | Task errored, timed out, or its agent disconnected. Payload includes the error. |

For finer-grained transitions (queued, running, cancelled) subscribe
to the task's [SSE event stream](/api-reference/streaming-events)
instead of webhooks.

## Payload shape

Each delivery is a JSON `POST`. Headers:

```http theme={null}
POST /your/path HTTP/1.1
Content-Type: application/json
X-Webhook-Signature-256: sha256=<hex_hmac>
```

Body:

```json theme={null}
{
  "event_type": "task.completed",
  "task_id": "019c65a0-aaaa-7bbb-cccc-dddddddddddd",
  "task_status": "completed",
  "task_output": {
    "storage_key": "tasks/019c65a0.../output.png",
    "image_count": 1
  },
  "task_error": "",
  "timestamp": "2026-02-16T09:00:00Z"
}
```

A `task.failed` delivery has the same shape with `task_status:
"failed"`, `task_output: null`, and `task_error` populated with the
failure reason.

| Field         | Meaning                                                                        |
| ------------- | ------------------------------------------------------------------------------ |
| `event_type`  | `task.completed` or `task.failed`.                                             |
| `task_id`     | UUID of the task that triggered the event.                                     |
| `task_status` | Terminal task status: `completed` or `failed`.                                 |
| `task_output` | Task output object on success (varies by workflow runtime), `null` on failure. |
| `task_error`  | Error string on failure, empty on success.                                     |
| `timestamp`   | ISO 8601 timestamp when the event was emitted.                                 |

## Verify the signature

Cyberun signs every delivery with HMAC-SHA256 using the secret shown
at webhook creation. **Verify the signature before trusting the
payload.**

The signature is computed over the raw request body and sent as
`X-Webhook-Signature-256: sha256=<hex_hmac>`.

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

function verify(body: Buffer, header: string, secret: string): boolean {
  const expected = createHmac('sha256', secret).update(body).digest('hex');
  const received = header.replace(/^sha256=/, '');
  return (
    expected.length === received.length &&
    timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(received, 'hex'))
  );
}
```

```python theme={null}
import hmac, hashlib

def verify(body: bytes, header: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", header)
```

Reject deliveries with missing or non-matching signatures with HTTP
`401`.

## Acknowledge promptly

* Respond `2xx` quickly. Slow responses count as failures and trigger
  the retry policy.
* Do the heavy work asynchronously. The handler should validate the
  signature, enqueue the work, and return — not block on downstream
  systems.

## Retries

On `5xx` or timeout, Cyberun retries up to **4 times** with
exponential backoff: immediate → 5s → 30s → 2min. After the last
attempt fails the delivery is dropped.

The `task_id` is stable across retries — your handler should treat a
repeat delivery for the same `task_id` and `event_type` as a no-op.

## Rotate the signing secret

If a secret is suspected leaked, open the webhook's detail page and
rotate it. The new secret is shown once; future deliveries are signed
with it immediately.

## Don't

* ❌ Trust the payload without verifying the signature.
* ❌ Block on slow work in the request handler — acknowledge first,
  process after.
* ❌ Expose the signing secret in client-side code. It's a server-side
  secret.

## Related

* [Tasks](/cloud/concepts/tasks) — the events you'll most often react to.
* [Stream task events (SSE)](/api-reference/streaming-events) — for
  per-step progress, not just terminal events.
* [API reference](/api-reference/introduction) — webhook management endpoints.
