Skip to main content

Webhook Callback Contract

Describes the inbound HTTP request the platform sends to your callback URL when delivering events: request method and headers, the signature verification algorithm, event envelope fields, and idempotency, retry, and delivery guarantees. The event body carries only resource identifiers; query the latest state via data.id.

Overview

After you create a Webhook and subscribe to named events, the platform delivers a notification to the configured callback URL whenever an event occurs. This page describes the inbound request contract the platform sends: your receiver only needs to verify the signature per this contract, parse the event envelope, and query the latest resource state via data.id. The event body does not carry the full resource content — only the event type, resource identifiers, and essential context. The receiver queries the latest resource by calling the corresponding GET endpoint with data.id.

Supported events

events supports the following 32 named events. Wildcard subscriptions such as * are not supported.
CategoryEvents
Session management planesession.created, session.updated, session.archived, session.deleted
Session run statussession.status_run_started, session.status_idled, session.status_terminated
Session Threadsession.thread_created, session.thread_run_started, session.thread_idled, session.thread_terminated
Agentagent.created, agent.updated, agent.archived
Deploymentdeployment.created, deployment.updated, deployment.archived, deployment.paused, deployment.unpaused
Deployment Rundeployment_run.started, deployment_run.failed, deployment_run.succeeded
Environmentenvironment.created, environment.updated, environment.archived, environment.deleted
Vaultvault.created, vault.archived, vault.deleted
Vault Credentialvault_credential.created, vault_credential.archived, vault_credential.deleted

Callback request

The platform sends a POST request to an HTTP or HTTPS address that passes the security check, and does not follow redirects. A public IP entered directly is allowed for delivery; private, loopback, link-local, and reserved addresses are refused. Each delivery regenerates webhook-timestamp and computes the signature using the current signing secret. On retries, the event body, outer id, and created_at remain unchanged. Request headers example:
POST /managedagent/webhooks HTTP/1.1
Host: example.com
Content-Type: application/json
User-Agent: Bailian-ManagedAgent-Webhook/1.0
webhook-id: whe_01JXX8JY9BBM4BK4C2P7K3M3ZR
webhook-timestamp: 1785810621
webhook-signature: v1,BASE64_HMAC_SHA256
HeaderDescription
Content-TypeAlways application/json
User-AgentAlways Bailian-ManagedAgent-Webhook/1.0
webhook-idThe outer event.id, format whe_<ULID>; used for idempotent deduplication and as part of the signed content
webhook-timestampUnix second-precision timestamp generated for this delivery; regenerated on each delivery
webhook-signatureSignature, format v1, plus the Base64-encoded HMAC-SHA256 result

Signature verification

Signed content and algorithm:
signed_content = webhook-id + "." + webhook-timestamp + "." + raw_body
secret_bytes = Base64Decode(RemovePrefix(signing_secret, "whsec_"))
signature = Base64(HMAC-SHA256(secret_bytes, UTF8(signed_content)))
  • The value of webhook-id is the outer event.id, not the webhook_id of the Webhook configuration.
  • signing_secret is whsec_ plus standard Base64 text. When verifying, strip the whsec_ prefix and decode the remainder with standard Base64; do not use the full whsec_... string directly as the HMAC key.
  • Read the unmodified raw request body to verify the signature first, then perform JSON deserialization.
  • Verify that the timestamp is within 5 minutes of the current time, and use a constant-time comparison to verify the signature.
You must verify the signature against the unparsed raw request body, then deserialize the JSON. Any re-encoding or parsing of the body before verification will cause the signature to mismatch.
The signing secret is returned only in the successful responses of Create and reset-secret; later queries do not return it. Store it securely.

Event envelope fields

A regular event body:
{
  "type": "event",
  "id": "whe_01JXX8JY9BBM4BK4C2P7K3M3ZR",
  "created_at": "2026-08-06T10:30:21.123Z",
  "data": {
    "id": "sesn_xxx",
    "type": "session.status_idled",
    "workspace_id": "ws_xxx"
  }
}
Thread events additionally carry session_thread_id: data.id is the session identifier, and data.session_thread_id is the specific thread identifier. session.thread_created, session.thread_run_started, session.thread_idled, and session.thread_terminated all use this structure:
{
  "type": "event",
  "id": "whe_01JXX8JY9BBM4BK4C2P7K3M3ZR",
  "created_at": "2026-08-06T10:30:21.123Z",
  "data": {
    "id": "sesn_xxx",
    "type": "session.thread_idled",
    "workspace_id": "ws_xxx",
    "session_thread_id": "sthread_xxx"
  }
}
FieldTypeDescription
typestringAlways event
idstringEvent ID, format whe_<ULID>; all delivery attempts for the same event share the same outer id, matching the webhook-id header
created_atstringEvent creation time, ISO 8601
dataobjectEvent payload
data.idstringResource identifier; for Thread events, this is the Session identifier
data.typestringEvent type
data.workspace_idstringWorkspace ID
data.session_thread_idstringFor Thread events, the specific Thread identifier
data.vault_idstringCarried in Vault Credential events

Idempotency, retry, and delivery guarantees

The receiver does not need to return a response body and should return a status code within 5 seconds. The platform decides subsequent behavior based on the receiver's response or error:
Receiver response or errorPlatform behavior
200–299Delivery succeeded; no retry
300–399Redirects are not followed; no retry; the current Webhook is disabled immediately
408, 425, 429Current request failed; enters retry
Other 400–499 statusesCurrent delivery ultimately failed; no retry
500–599Current request failed; enters retry
DNS resolution, connection, write, or read timeoutCurrent request failed; enters retry
HTTPS certificate or hostname verification failureNo retry; the current Webhook is disabled immediately
Domain resolves to a private or reserved addressConnection is refused; the current Webhook is disabled immediately
After the first request for a business event fails, it is retried up to 3 more times, with wait intervals of 10s, 30s, and 1min — up to 4 actual network requests in total. If the 4th attempt still fails, a final failure is recorded. By default, a Webhook is auto-disabled after 20 consecutive business events ultimately fail. webhook.test sends a single synchronous request only, is not retried, and does not affect the consecutive-failure count. Other delivery semantics:
  • Possible duplicates: The same event may be delivered multiple times, always with the same event.id; deduplicate idempotently by that identifier.
  • No ordering guarantee: Events may arrive out of order; sort by created_at when ordering is required, and treat the resource query result as the source of truth for final state.
  • Query retention: Delivered events are retained for 7 days, after which they can no longer be queried.

Verification example

import base64
import hashlib
import hmac
import os
import time

secret = os.environ["AGENT_WEBHOOK_SECRET"]
secret_bytes = base64.b64decode(secret[len("whsec_"):])

def verify_webhook(raw_body: bytes, webhook_id: str, timestamp: str, signature_header: str) -> bool:
    try:
        if abs(time.time() - int(timestamp)) > 300:
            return False

        version, signature = signature_header.split(",", 1)
        if version != "v1":
            return False

        signed_payload = f"{webhook_id}.{timestamp}.".encode() + raw_body
        expected = base64.b64encode(
            hmac.new(secret_bytes, signed_payload, hashlib.sha256).digest()
        ).decode()
        return hmac.compare_digest(signature, expected)
    except (TypeError, ValueError):
        return False
Flow Agent API
Sandbox API
RAG API
Long-term Memory
ParseX API
Security API
Framework Integration