Skip to main content
A webhook is an HTTPS URL you register for your organization. When a session 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. Each one has a target url, a list of enabled_events, and a signing secret returned once at creation.

Events

Event typeSent when
session.status_updatedA session’s status changes, e.g. runningcompleted.
Subscribe to specific types via enabled_events, or use "*" to receive all current and future event types.

Delivery payload

Each delivery is a POST with a JSON body:
{
  "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"
  }
}
type
string
The event type, e.g. session.status_updated.
id
string
Unique id for this event.
created_at
string
When the status change occurred (UTC, RFC 3339, millisecond precision).
data
object
Event payload: session_id, the new status (pending, running, paused, idle, awaiting_tool_results, completed, failed, timed_out, or interrupted), and the previous_status it transitioned from (null when unknown).

Verifying deliveries

Every delivery carries two headers:
HeaderValue
X-H-Webhook-TimestampUnix timestamp (seconds) of the delivery.
X-H-Webhook-Signaturesha256= + hex HMAC-SHA256 of {timestamp}.{raw_body}, keyed with your webhook’s secret.
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.
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}
Verify against the raw request body bytes, exactly as received. Parsing and re-serializing the JSON changes the bytes and invalidates the signature.
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-most-once: a single POST per event with a 10-second timeout, and no retries. Treat a webhook as a trigger rather than a source of truth. On receipt, fetch the authoritative state with Get session status. If your endpoint is briefly unavailable, you may miss an event, so keep a polling fallback for critical flows. Out-of-order arrival is possible when statuses change in quick succession; previous_status lets you detect and discard stale transitions.

Constraints

  • Target URLs must be https:// and resolve to a public address; deliveries to private or internal hosts are skipped.
  • The signing secret is returned only in the Create response. To rotate it, delete the webhook and create a new one.
  • An organization can register up to 10 webhooks.
  • A disabled webhook stays registered but receives no deliveries.