Reference
Webhook Events
Every event the engine emits, with field-by-field payload documentation. All webhooks are HMAC-signed, delivered with exponential-backoff retry, and audit-logged in webhook_deliveries so you can replay or diagnose any delivery.
Subscribe
Create a webhook subscription:
curl -X POST https://humaneai.vaarak.com/api/webhooks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"label": "On-call alerts",
"url": "https://yourapp.example/webhook",
"events": "proactive.*,safety.blocked"
}'signing_secret (awhsec_…string) — store it now. You'll use it to verify the X-Humane-Signature header on every incoming delivery.Event filters
The events field takes a comma-separated list. Supported patterns:
*— everythingproactive.*— prefix wildcardsafety.blocked,safety.hold— explicit list
Verifying signatures
Every delivery carries X-Humane-Signature: t=<unix>,v1=<hex>. Compute HMAC-SHA256(secret, "{t}.{raw_body}") and compare constant-time. Reject timestamps older than 5 minutes (replay protection).
# Python — standard library only (hmac + hashlib)
import hmac, hashlib, time, json
def verify(secret: str, raw: bytes, header: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t, v1 = parts.get("t", ""), parts.get("v1", "")
if not t or not v1 or abs(time.time() - int(t)) > 300: # replay window
return False
expected = hmac.new(
secret.encode(), f"{t}.{raw.decode()}".encode(), hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, v1) # constant-time
@app.post("/webhook")
async def receive(request: Request):
raw = await request.body()
sig = request.headers.get("X-Humane-Signature", "")
if not verify(MY_SIGNING_SECRET, raw, sig):
raise HTTPException(401, "invalid signature")
event = json.loads(raw)
...Core events
interaction.processedEvery successful SDK /process call.
{
"event": "interaction.processed",
"timestamp": "2026-04-17T14:22:10Z",
"data": {
"user_id": "u_8f2",
"safety_action": "PROCEED",
"mood": 0.48,
"energy": 0.51,
"interaction_count": 7
}
}user.createdFirst time an end_user appears for this tenant.
{
"event": "user.created",
"timestamp": "2026-04-17T14:22:10Z",
"data": {
"user_id": "u_8f2",
"channel": "ios_app",
"timestamp": "2026-04-17T14:22:10Z"
}
}user.sentiment_shiftSentiment changes by more than 0.2 in a single interaction.
{
"event": "user.sentiment_shift",
"timestamp": "2026-04-17T14:22:10Z",
"data": {
"user_id": "u_8f2",
"old_sentiment": 0.62,
"new_sentiment": 0.31,
"delta": 0.31,
"direction": "negative"
}
}Safety events
safety.flaggedRisk crosses elevated or critical on an observe() or /process call — even when the response is allowed to proceed. This is the headline alert for the 'page a human when a user is in crisis' flow.
{
"event": "safety.flagged",
"timestamp": "2026-04-17T14:22:10Z",
"data": {
"user_id": "u_8f2",
"end_user_id": "8c1d…",
"risk_level": "critical",
"flags": ["self_harm", "passive_si"],
"source": "observe"
}
}safety.blockedPre-LLM safety gate returns BLOCK. The LLM is never called.
{
"event": "safety.blocked",
"timestamp": "2026-04-17T14:22:10Z",
"data": {
"user_id": "u_8f2",
"risk_score": 0.92,
"flags": ["self_harm"],
"message_preview": "I don't want to..."
}
}safety.holdAmbiguous message — flagged for human review but not blocked.
{
"event": "safety.hold",
"timestamp": "2026-04-17T14:22:10Z",
"data": {
"user_id": "u_8f2",
"risk_score": 0.58,
"flags": ["legal_advice_request"]
}
}Proactive events
proactive.mood_dropEnd-user mood drops below 0.3.
{
"event": "proactive.mood_drop",
"timestamp": "2026-04-17T14:22:10Z",
"data": {
"trigger": "mood_drop",
"description": "User mood dropped below critical threshold",
"suggested_action": "Send empathetic check-in message",
"end_user_id": "u_8f2",
"user_state": {
"mood": 0.18, "energy": 0.42, "trust": 0.55,
"interaction_count": 7,
"last_seen": "2026-04-17T14:19:40Z"
},
"timestamp": "2026-04-17T14:22:10Z"
}
}proactive.energy_lowEnd-user energy drops below 0.25.
{
"event": "proactive.energy_low",
"timestamp": "...",
"data": { "trigger": "energy_low", "end_user_id": "...", "user_state": { ... } }
}proactive.trust_milestoneEnd-user crosses trust > 0.7 with more than 10 interactions.
{
"event": "proactive.trust_milestone",
"timestamp": "...",
"data": { "trigger": "trust_milestone", "end_user_id": "...", "user_state": { ... } }
}proactive.user_inactiveEnd-user has not interacted in more than 48 hours.
{
"event": "proactive.user_inactive",
"timestamp": "...",
"data": { "trigger": "user_inactive", "end_user_id": "...", "user_state": { ... } }
}proactive.policy_{rule_name}Any policy with a fire_event action matched.
{
"event": "proactive.policy_sustained_distress",
"timestamp": "...",
"data": {
"user_id": "u_8f2",
"policy_rule": "sustained_distress",
"mood": 0.22, "energy": 0.4, "trust": 0.55
}
}Takeover events
Fired across the human-takeover lifecycle. Every payload carries your external user_id plus end_user_id, session_id, operator_id, operator_name, and state; takeover.message additionally includes the body. See Concepts → Human Takeover for the full flow and the /process suppression contract.
takeover.startedA human operator opens a takeover session (fires only on a genuine open, not on a no-op re-start).
{
"event": "takeover.started",
"timestamp": "2026-06-02T14:22:10Z",
"data": {
"user_id": "u_8f2",
"end_user_id": "8c1d…",
"session_id": "1f2e…",
"operator_id": "op_1",
"operator_name": "Dana",
"state": "active"
}
}takeover.messageAn operator injects a turn into the live conversation. Same base fields plus the message body + id.
{
"event": "takeover.message",
"timestamp": "2026-06-02T14:22:30Z",
"data": {
"user_id": "u_8f2",
"end_user_id": "8c1d…",
"session_id": "1f2e…",
"operator_id": "op_1",
"operator_name": "Dana",
"state": "active",
"message_id": "9a7b…",
"body": "Hey, it's Dana — I'm here.",
"created_at": "2026-06-02T14:22:30Z"
}
}takeover.releasedThe session is handed back to the AI (manually via release).
{
"event": "takeover.released",
"timestamp": "2026-06-02T14:40:00Z",
"data": {
"user_id": "u_8f2",
"end_user_id": "8c1d…",
"session_id": "1f2e…",
"operator_id": "op_1",
"operator_name": "Dana",
"state": "released",
"handback_message": "Handing you back to the assistant."
}
}takeover.expiredThe idle window elapsed and the session auto-released without an explicit hand-back.
{
"event": "takeover.expired",
"timestamp": "2026-06-02T14:52:10Z",
"data": {
"user_id": "u_8f2",
"end_user_id": "8c1d…",
"session_id": "1f2e…",
"operator_id": "op_1",
"operator_name": "Dana",
"state": "released"
}
}Audit trail
Every fired event is persisted in webhook_deliverieswhether or not a receiver was subscribed. Query them via:
curl -H "X-API-Key: hx_..." \
"https://humaneai.vaarak.com/api/webhook-events?since_hours=168&limit=100"Returns each event with delivery status and the full payload. Useful for the Signals UI, incident forensics, or a "re-deliver failed webhook" flow.