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

# Get notified with webhooks

> Receive signed HTTP notifications when your sessions change status.

export const Webhooks = () => {
  const ArrowRight = ({label}) => <div className="flex shrink-0 flex-col items-center justify-center gap-1 px-2.5 py-1 md:py-0">
      {label ? <span className="whitespace-nowrap text-[10.5px] text-zinc-400 dark:text-zinc-500">{label}</span> : null}
      <svg className="h-4 w-7 rotate-90 text-zinc-400 dark:text-zinc-500 md:rotate-0" viewBox="0 0 28 16" fill="none" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round"><path d="M1 8h23" /><path d="M19 3l6 5-6 5" /></svg>
    </div>;
  const StatusChip = ({children}) => <span className="rounded-md bg-zinc-100 px-2 py-0.5 font-mono text-[11px] text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300">{children}</span>;
  const sessionIcon = <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>;
  const platformIcon = <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" /><rect width="20" height="8" x="2" y="14" rx="2" /><path d="M6 6h.01" /><path d="M6 18h.01" /></svg>;
  const endpointIcon = <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10" /><path d="m9 12 2 2 4-4" /></svg>;
  return <div className="not-prose my-6 overflow-x-auto">
      <div className="flex min-w-[600px] flex-col items-stretch md:flex-row md:items-center">
        <div className="w-[128px] shrink-0 rounded-xl border border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-950">
          <div className="mb-3 flex items-center gap-2 text-[13.5px] font-medium text-zinc-900 dark:text-zinc-100">
            <span className="text-zinc-500 dark:text-zinc-400">{sessionIcon}</span>
            <span className="whitespace-nowrap">Session</span>
          </div>
          <div className="flex flex-col items-start gap-1.5">
            <StatusChip>running</StatusChip>
            <svg className="ml-2 h-3.5 w-3.5 text-zinc-400 dark:text-zinc-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 5v14" /><path d="m19 12-7 7-7-7" /></svg>
            <StatusChip>completed</StatusChip>
          </div>
        </div>

        <ArrowRight label="status change" />

        <div className="flex-1 rounded-xl border border-transparent bg-zinc-900 p-4 dark:bg-zinc-100">
          <div className="mb-3 flex items-center gap-2 text-[13.5px] font-medium text-white dark:text-zinc-900">
            <span className="text-zinc-400 dark:text-zinc-500">{platformIcon}</span>
            <span className="whitespace-nowrap">H Platform</span>
          </div>
          <pre className="overflow-x-auto rounded-lg bg-zinc-800 p-2.5 font-mono text-[10px] leading-[1.5] text-zinc-300 dark:bg-zinc-200 dark:text-zinc-600">{`{
  "type": "session.status_updated",
  "data": { "status": "completed" }
}`}</pre>
          <div className="mt-2.5 flex flex-wrap gap-1.5">
            <span className="whitespace-nowrap rounded-md bg-zinc-800 px-1.5 py-0.5 font-mono text-[10px] text-zinc-400 dark:bg-zinc-200 dark:text-zinc-500">X-H-Webhook-Signature</span>
            <span className="whitespace-nowrap rounded-md bg-zinc-800 px-1.5 py-0.5 font-mono text-[10px] text-zinc-400 dark:bg-zinc-200 dark:text-zinc-500">X-H-Webhook-Timestamp</span>
          </div>
        </div>

        <ArrowRight />

        <div className="w-[164px] shrink-0 rounded-xl border border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-950">
          <div className="mb-3 flex items-center gap-2 text-[13.5px] font-medium text-zinc-900 dark:text-zinc-100">
            <span className="text-zinc-500 dark:text-zinc-400">{endpointIcon}</span>
            <span className="whitespace-nowrap">Your endpoint</span>
          </div>
          <div className="text-[11.5px] leading-4 text-zinc-400 dark:text-zinc-500">verify the signature, then <span className="font-mono text-zinc-500 dark:text-zinc-400">GET /status</span> for the truth</div>
        </div>
      </div>
    </div>;
};

<Webhooks />

A webhook is an HTTPS URL you register for your organization. When a [session](/computer-use-agents/sessions/overview) changes status, the platform sends a signed `POST` request to every subscribed webhook, so you can react to completions and failures without polling.

Manage webhooks with the [CRUD API](/computer-use-agents/webhooks/create). Each one has a target `url`, a list of `enabled_events`, and a signing `secret` returned once at creation.

## Events

| Event type                      | Sent when                                                                               |
| ------------------------------- | --------------------------------------------------------------------------------------- |
| `session.status_updated`        | A session's status changes, e.g. `running` → `completed`.                               |
| `session.completed`             | A session finishes successfully.                                                        |
| `session.failed`                | A session fails.                                                                        |
| `session.timed_out`             | A session exceeds its time limit.                                                       |
| `session.idle`                  | The agent answered and is waiting for the next message.                                 |
| `session.awaiting_tool_results` | The agent is waiting for [client-side tool results](/computer-use-agents/custom-tools). |

All event types share the same payload shape. `"*"` subscribes to the `session.status_updated` firehose. Each type in `enabled_events` is delivered independently: subscribing to both `session.status_updated` and `session.failed` gets you two deliveries when a session fails, one per type. List the available types programmatically with [List event types](/computer-use-agents/webhooks/events).

## Delivery payload

Each delivery is a `POST` with a JSON body:

```json Delivery payload theme={null}
{
  "type": "session.status_updated",
  "id": "evt_5d1f0c9e8a7b4c2da93f1e6b8c4d2a70",
  "created_at": "2026-06-11T15:04:05.123Z",
  "data": {
    "session_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "status": "completed",
    "previous_status": "running"
  }
}
```

<ResponseField name="type" type="string">
  The event type, e.g. `session.status_updated`.
</ResponseField>

<ResponseField name="id" type="string">
  Unique id for this event.
</ResponseField>

<ResponseField name="created_at" type="string">
  When the status change occurred (UTC, RFC 3339, millisecond precision).
</ResponseField>

<ResponseField name="data" type="object">
  Event payload: `session_id`, the new `status` (`queued`, `pending`, `running`, `paused`, `idle`, `awaiting_tool_results`, `completed`, `failed`, `timed_out`, or `interrupted`), and the `previous_status` it transitioned from (`null` when unknown).
</ResponseField>

## Verifying deliveries

Every delivery carries these headers:

| Header                  | Value                                                                                        |
| ----------------------- | -------------------------------------------------------------------------------------------- |
| `X-H-Webhook-Timestamp` | Unix timestamp (seconds) of the delivery attempt.                                            |
| `X-H-Webhook-Signature` | `sha256=` + hex HMAC-SHA256 of `{timestamp}.{raw_body}`, keyed with your webhook's `secret`. |
| `X-H-Webhook-Delivery`  | Unique id for this delivery, stable across retries.                                          |

Always verify before trusting a delivery. The SDKs ship a helper that checks the signature, rejects stale deliveries (older than 5 minutes by default), and parses the event.

<CodeGroup>
  ```python Python (FastAPI) theme={null}
  from fastapi import FastAPI, Header, HTTPException, Request
  from hai_agents import WebhookEventData, WebhookVerificationError, verify_webhook

  app = FastAPI()

  @app.post("/hooks/h")
  async def receive(
      request: Request,
      x_h_webhook_signature: str = Header(""),
      x_h_webhook_timestamp: str = Header(""),
  ):
      body = await request.body()
      try:
          event = verify_webhook(body, x_h_webhook_signature, x_h_webhook_timestamp, secret="whsec_...")
      except WebhookVerificationError:
          raise HTTPException(status_code=400, detail="invalid signature")
      # event.data is the raw payload; its shape depends on event.type.
      if event.type == "session.status_updated":
          data = WebhookEventData.model_validate(event.data)
          if data.status == "completed":
              print(f"session {data.session_id} finished")
      return {"ok": True}
  ```

  ```typescript TypeScript (Express) theme={null}
  import express from "express";
  import { type WebhookEventData, WebhookVerificationError, verifyWebhook } from "hai-agents";

  const app = express();
  app.use("/hooks/h", express.raw({ type: "application/json" }));

  app.post("/hooks/h", (req, res) => {
    try {
      const event = verifyWebhook(
        req.body,
        req.header("X-H-Webhook-Signature") ?? "",
        req.header("X-H-Webhook-Timestamp") ?? "",
        "whsec_...",
      );
      // event.data is the raw payload; its shape depends on event.type.
      if (event.type === "session.status_updated") {
        const data = event.data as unknown as WebhookEventData;
        if (data.status === "completed") {
          console.log(`session ${data.session_id} finished`);
        }
      }
      res.json({ ok: true });
    } catch (e) {
      if (e instanceof WebhookVerificationError) {
        res.status(400).json({ error: "invalid signature" });
        return;
      }
      throw e;
    }
  });
  ```
</CodeGroup>

<Warning>
  Verify against the raw request body bytes, exactly as received. Parsing and re-serializing the JSON changes the bytes and invalidates the signature.
</Warning>

To verify manually: compute `HMAC-SHA256(secret, "{timestamp}." + raw_body)`, hex-encode it, prefix with `sha256=`, and compare it to `X-H-Webhook-Signature` using a constant-time comparison. Reject deliveries whose timestamp is more than a few minutes old to guard against replays.

## Delivery semantics

Deliveries are at-least-once, with a 10-second timeout per attempt. If your endpoint is unreachable or returns a non-2xx status, delivery is retried with increasing backoff, up to 8 attempts spanning about 24 hours, after which the event is dropped.

Because of retries:

* **Deduplicate.** The same event can arrive more than once. The event `id` and the `X-H-Webhook-Delivery` header are stable across retries; skip ids you have already processed.
* **Ignore arrival order.** A retried old event can land after a newer one. Trust the event's own `status`, `created_at`, and `previous_status`, not the order of arrival.
* **Return 2xx quickly.** Any other status counts as a failure and schedules a retry. Do slow work after responding.

Webhooks are a trigger rather than a source of truth: on receipt, fetch the authoritative state with [Get session status](/computer-use-agents/sessions/status).

Each webhook records the result of its latest delivery attempt: [Retrieve](/computer-use-agents/webhooks/retrieve) returns `last_delivery_status`, `last_delivery_error`, `last_delivery_at`, `last_success_at`, and `consecutive_failures`, so you can check whether an endpoint is healthy, and why it was disabled, without digging through receiver logs.

## Testing an endpoint

[Ping](/computer-use-agents/webhooks/ping) sends a signed `ping` event through the real delivery path and returns your endpoint's HTTP response synchronously, so you can validate URL, signature verification, and connectivity before relying on the webhook.

## Rotating the secret

[Rotate](/computer-use-agents/webhooks/rotate) replaces the signing secret without a verification gap:

1. Deploy your receiver passing **both** the current and a placeholder for the new secret to the verify helper (it accepts a list).
2. Call the rotate endpoint and store the new secret; update the receiver's secret list.
3. Once deliveries verify against the new secret, remove the old one.

## Constraints

* Target URLs must be `https://` and resolve to a public address. Reachability is verified with a `HEAD` request when you register or change the URL, and deliveries to private or internal hosts fail (and count as failed attempts).
* The signing `secret` is returned only by [Create](/computer-use-agents/webhooks/create) and [Rotate](/computer-use-agents/webhooks/rotate).
* An organization can register up to 10 webhooks.
* A `disabled` webhook stays registered but receives no deliveries.
* After 50 consecutive failed delivery attempts, a webhook is automatically disabled. Fix the receiver, then re-enable it with [Update](/computer-use-agents/webhooks/update) (`{"disabled": false}`).
