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:

bash
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"
  }'
The signing secret is shown once
The response includes 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:

  • * — everything
  • proactive.* — prefix wildcard
  • safety.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
# 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.processed

Every successful SDK /process call.

json
{
  "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.created

First time an end_user appears for this tenant.

json
{
  "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_shift

Sentiment changes by more than 0.2 in a single interaction.

json
{
  "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.flagged

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

info
user_id is your EXTERNAL id; end_user_id is the internal uuid. source is the path that fired it — "observe" or "process". Unlike safety.blocked, this fires even when the turn proceeds, so wire it to your on-call channel.
json
{
  "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.blocked

Pre-LLM safety gate returns BLOCK. The LLM is never called.

info
Always wire this up to a human channel. Slack / Discord integrations are the fastest path.
json
{
  "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.hold

Ambiguous message — flagged for human review but not blocked.

json
{
  "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_drop

End-user mood drops below 0.3.

json
{
  "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_low

End-user energy drops below 0.25.

json
{
  "event": "proactive.energy_low",
  "timestamp": "...",
  "data": { "trigger": "energy_low", "end_user_id": "...", "user_state": { ... } }
}
proactive.trust_milestone

End-user crosses trust > 0.7 with more than 10 interactions.

json
{
  "event": "proactive.trust_milestone",
  "timestamp": "...",
  "data": { "trigger": "trust_milestone", "end_user_id": "...", "user_state": { ... } }
}
proactive.user_inactive

End-user has not interacted in more than 48 hours.

json
{
  "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.

info
The suffix after 'proactive.' is your rule name. Subscribe with a wildcard: proactive.*
json
{
  "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.started

A human operator opens a takeover session (fires only on a genuine open, not on a no-op re-start).

info
user_id is your EXTERNAL id (the path param), so you can correlate without an id mapping. end_user_id is the internal uuid.
json
{
  "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.message

An operator injects a turn into the live conversation. Same base fields plus the message body + id.

json
{
  "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.released

The session is handed back to the AI (manually via release).

json
{
  "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.expired

The idle window elapsed and the session auto-released without an explicit hand-back.

info
Configure the window per session with idle_minutes on takeover.start.
json
{
  "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:

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