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

> ## Agent Instructions
> H Platform has four products: the Agents API (managed computer-use agents, base URL https://agp.eu.hcompany.ai/api/v2 or https://agp.hcompany.ai/api/v2 for the US), the Models API (OpenAI-compatible Holo vision-language models at https://api.hcompany.ai/v1), HoloDesktop CLI (Holo on the user's own desktop), and HoloTab (a free no-code Chrome extension that runs Holo in the user's browser, with recordable routines and schedules).
> Authenticate with a bearer API key from the HAI_API_KEY environment variable. SDKs: `pip install hai-agents` (Python, `from hai_agents import Client`) and `npm install hai-agents` (TypeScript, `import { HaiAgentsClient } from "hai-agents"`). CLI: `hai`.
> Agents do work in a browser or on a desktop; describe the task as an imperative instruction. To run a task quickly, prefer the pre-built agent `h/web-surfer-flash`. Read results from the session's `latest_answer` after it reaches a terminal status.
> Sessions are the unit of work; wait for a terminal status (completed, failed, timed_out, interrupted) before reading the answer. Use webhooks or the `changes` long-poll endpoint to follow progress.

# Get notified with webhooks

> Receive signed HTTP notifications when sessions change status, verify signatures, and rotate secrets.

export const Webhooks = () => {
  const stroke = {
    fill: "none",
    stroke: "currentColor",
    strokeWidth: 1.75,
    strokeLinecap: "round",
    strokeLinejoin: "round"
  };
  const S = c => ({
    className: c,
    ...stroke
  });
  const icons = {
    session: c => <svg viewBox="0 0 24 24" {...S(c)}><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>,
    platform: c => <svg viewBox="0 0 24 24" {...S(c)}><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.01M6 18h.01" /></svg>,
    endpoint: c => <svg viewBox="0 0 24 24" {...S(c)}><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>
  };
  const Card = ({icon, title, sub, children}) => <div className="flex shrink-0 flex-col self-center rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-800 dark:bg-zinc-950">
      <div className="flex items-center gap-2.5">
        <span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-200">{icon("h-5 w-5")}</span>
        <div>
          <div className="whitespace-nowrap text-base font-semibold leading-6 text-zinc-900 dark:text-zinc-100">{title}</div>
          {sub && <div className="whitespace-nowrap text-sm text-zinc-500 dark:text-zinc-400">{sub}</div>}
        </div>
      </div>
      {children}
    </div>;
  const Arrow = ({label}) => <div className="flex min-w-[72px] flex-1 flex-col items-stretch justify-center gap-1 px-3 text-center text-xs leading-4 text-zinc-500 dark:text-zinc-400">
      <span className="whitespace-nowrap">{label || "\u00a0"}</span>
      <div className="flex items-center text-zinc-400 dark:text-zinc-600">
        <span className="h-px flex-1 bg-current" />
        <svg className="-ml-px h-3 w-2 shrink-0" viewBox="0 0 8 12" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M1 1.5 6 6l-5 4.5" /></svg>
      </div>
    </div>;
  const Chip = ({children}) => <span className="whitespace-nowrap rounded-md bg-zinc-100 px-2 py-0.5 font-mono text-xs text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300">{children}</span>;
  return <div className="not-prose my-8 overflow-x-auto">
      <div className="flex min-w-[680px] items-stretch">
        <Card icon={icons.session} title="Session" sub="status changes">
          <div className="mt-4 flex flex-col items-start gap-1.5">
            <Chip>running</Chip>
            <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>
            <Chip>completed</Chip>
          </div>
        </Card>

        <Arrow label="event" />

        <Card icon={icons.platform} title="H Platform" sub="signs and sends">
          <pre className="mt-4 rounded-lg bg-zinc-100 p-3 font-mono text-[11px] leading-[1.6] text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300">{`POST /hooks
X-H-Webhook-Signature: ...
X-H-Webhook-Timestamp: ...

{ "type": "session.status_updated",
  "data": { "status": "completed" } }`}</pre>
        </Card>

        <Arrow label="HTTPS" />

        <Card icon={icons.endpoint} title="Your endpoint" sub="verifies, then acts">
          <div className="mt-4 flex flex-col gap-2 text-sm text-zinc-600 dark:text-zinc-400">
            <div>1. Check the signature</div>
            <div>2. <span className="font-mono text-[13px]">GET /status</span> for the truth</div>
          </div>
        </Card>
      </div>
    </div>;
};

export const Notice = ({kind = "note", title, children}) => {
  const kinds = {
    warning: {
      label: "User notice",
      icon: <>
          <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
          <path d="M12 9v4" />
          <path d="M12 17h.01" />
        </>
    },
    gotcha: {
      label: "Gotcha",
      icon: <>
          <circle cx="12" cy="12" r="10" />
          <path d="M12 16v-4" />
          <path d="M12 8h.01" />
        </>
    },
    note: {
      label: "Note",
      icon: <>
          <circle cx="12" cy="12" r="10" />
          <path d="M12 16v-4" />
          <path d="M12 8h.01" />
        </>
    }
  };
  const k = kinds[kind];
  return <div className="notice my-6 rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-800 dark:bg-zinc-950">
      <div className={`${kind === "warning" ? "not-prose flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-red-400/80 dark:text-red-400/70" : kind === "gotcha" ? "not-prose flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-amber-500/80 dark:text-amber-400/70" : "not-prose flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-zinc-400 dark:text-zinc-500"}`}>
        <svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          {k.icon}
        </svg>
        {k.label}
      </div>
      {title && <div className="not-prose mt-2 text-base font-semibold text-zinc-900 dark:text-zinc-100">{title}</div>}
      <div className="notice-body mt-3 text-sm leading-6 text-zinc-700 dark:text-zinc-300">{children}</div>
    </div>;
};

<Webhooks />

A webhook is an HTTPS URL registered for your organization. When a [session](/agents-api/sessions/overview) changes status, the platform sends a signed `POST` to every subscribed webhook, so you can react without polling. Each webhook has a `url`, a list of `enabled_events`, and a signing `secret` returned once at [creation](/agents-api/webhooks/create).

## Events

| Event type | Sent when |
| - | - |
| `session.status_updated` | A session's status changes, e.g. `running` to `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 waits for the next message. |
| `session.awaiting_tool_results` | The agent waits for [client-side tool results](/agents-api/custom-tools). |

* `"*"` subscribes to the `session.status_updated` firehose.
* Each subscribed type is delivered separately: with both `session.status_updated` and `session.failed`, a failure sends two deliveries.
* List the types programmatically with [List event types](/agents-api/webhooks/events).

## Delivery payload

All event types share this shape:

```json Delivery payload theme={"system"}
{
  "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"
  }
}
```

`created_at` is when the status changed (UTC, RFC 3339, milliseconds). `status` is any [session state](/agents-api/sessions/overview#lifecycle). `previous_status` is `null` when unknown.

## Verifying deliveries

| 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 the webhook's `secret`. |
| `X-H-Webhook-Delivery` | Unique delivery id, stable across retries. |

Always verify before trusting a delivery. The SDK helper checks the signature, rejects deliveries older than 5 minutes (tune with `tolerance_s` / `toleranceS`), and parses the event. On failure it raises `WebhookVerificationError`. Respond with `400`.

<CodeGroup>
  ```python Python theme={"system"}
  from hai_agents import verify_webhook

  event = verify_webhook(raw_body, signature_header, timestamp_header, secret="whsec_...")
  if event.type == "session.status_updated":
      print(event.data["session_id"], event.data["status"])
  ```

  ```typescript TypeScript theme={"system"}
  import { verifyWebhook, type WebhookEventData } from "hai-agents";

  const event = verifyWebhook(rawBody, signatureHeader, timestampHeader, "whsec_...");
  if (event.type === "session.status_updated") {
    const data = event.data as unknown as WebhookEventData;
    console.log(data.session_id, data.status);
  }
  ```
</CodeGroup>

Without an SDK: compute `HMAC-SHA256(secret, "{timestamp}." + raw_body)`, hex-encode it, prefix `sha256=`, compare it to `X-H-Webhook-Signature` in constant time, and reject timestamps more than a few minutes old.

<Notice kind="gotcha" title="Verify the raw bytes">
  Check the body exactly as received (for example `await request.body()` in FastAPI, `express.raw()` in Express). Parsing and re-serializing the JSON changes the bytes and breaks the signature.
</Notice>

## Delivery semantics

Deliveries are at-least-once, with a 10-second timeout per attempt. An unreachable endpoint or a non-2xx response is retried with increasing backoff, up to 8 attempts over about 24 hours, then the event is dropped. So:

* **Deduplicate.** The event `id` and `X-H-Webhook-Delivery` are stable across retries.
* **Ignore arrival order.** A retried old event can land after a newer one. Trust `status`, `previous_status`, and `created_at`.
* **Return 2xx fast.** Do slow work after responding.
* **Treat it as a trigger.** Fetch the authoritative state with [Get session status](/agents-api/sessions/status).

[Retrieve](/agents-api/webhooks/retrieve) reports each webhook's latest delivery (`last_delivery_status`, `last_delivery_error`, `last_delivery_at`, `last_success_at`, `consecutive_failures`), so you can check its health without reading receiver logs.

## Testing an endpoint

[Ping](/agents-api/webhooks/ping) sends a signed `ping` event through the real delivery path and returns your endpoint's response synchronously. Use it to check the URL, signature verification, and connectivity.

## Rotating the secret

[Rotate](/agents-api/webhooks/rotate) returns a new secret, and deliveries are signed with it from then on. The verify helpers accept a list of secrets for the switch:

1. Pass `["whsec_old", "whsec_new"]` to the verify helper as soon as you have the new secret. Deliveries rejected in between are retried.
2. Once deliveries verify against the new secret, remove the old one.

## Constraints

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

Related: [Schedules](/agents-api/schedules/overview), [Watch and steer sessions](/agents-api/observe-and-steer)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.