Webhooks
FundOS pushes real-time events to your agent's HTTPS endpoint. Webhooks are registered per OAuth client and signed with HMAC-SHA256 so you can verify every delivery.
Register an endpoint
curl -X POST https://kela.com/api/v1/webhooks/ \
-H "Authorization: Bearer vdr_<your-key>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-agent.example.com/hooks/fundos",
"name": "My agent webhook",
"event_types": ["credit.low", "action.approval_required", "action.approved"]
}'secret field is returned only once
at registration time. Store it in your secrets manager immediately — it cannot be
retrieved later (only rotated).
Pass "event_types": null to subscribe to all events.
Verify the signature
Every delivery includes an X-FundOS-Signature header.
Always verify it before processing the payload.
import hmac, hashlib
def verify_fundos_signature(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected.encode(), header.encode())
# Flask example:
@app.route("/hooks/fundos", methods=["POST"])
def fundos_webhook():
raw = request.get_data()
sig = request.headers.get("X-FundOS-Signature", "")
if not verify_fundos_signature(raw, sig, WEBHOOK_SECRET):
return jsonify({"error": "invalid signature"}), 401
event = request.json
handle_event(event)
return jsonify({"received": True})Payload shape
{
"event": "action.approved",
"version": "1.0",
"delivery_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-05-01T10:00:00.000Z",
"data": { "..." }
}
delivery_id is stable across all retry attempts — use it to
deduplicate events in your handler.
Event catalogue
| Event | When it fires | Key data fields |
|---|---|---|
credit.low |
Balance drops ≤ 100 credits after a tool call | balance, threshold |
credit.exhausted |
Balance reaches 0 — tool call blocked | balance |
credit.granted |
Admin tops up credits | amount, new_balance, note |
action.approval_required |
Agent proposes a side-effect (write tool called) | action_id, run_id, kind, proposal |
action.approved |
GP approves and executes a proposed action | action_id, run_id, kind, result |
action.rejected |
GP rejects a proposed action | action_id, run_id, kind, reason |
action.expired |
Pending action auto-expired after threshold | action_id, action_type, expired_at, expiry_hours |
job.completed |
AI-heavy tool returns successfully | run_id, agent_name, output |
job.failed |
AI-heavy tool raises an exception | run_id, agent_name, error |
module.enabled |
Org admin enables a module | module |
module.disabled |
Org admin disables a module | module |
Retry schedule
Failed deliveries (non-2xx response or network error) are retried automatically:
| Attempt | Delay after previous failure |
|---|---|
| 1 (immediate) | — |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 (final) | 8 hours |
After 6 failures the delivery is marked failed and the endpoint's failure count is incremented.
Management endpoints
ping.test delivery
Pending action expiry
Proposed actions that are not approved or rejected within a configurable window are
automatically expired. The default window is 24 hours; write-sensitive
actions (capital calls, transactions) use longer windows. When an action expires,
action.expired fires so your agent can decide whether to retry, escalate,
or abort the workflow.