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"]
  }'
Save the secret. The 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

EventWhen it firesKey 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:

AttemptDelay after previous failure
1 (immediate)
21 minute
35 minutes
430 minutes
52 hours
6 (final)8 hours

After 6 failures the delivery is marked failed and the endpoint's failure count is incremented.

Management endpoints

POST /api/v1/webhooks/ Register a new endpoint
GET /api/v1/webhooks/ List all endpoints (secret not returned)
PATCH /api/v1/webhooks/<id> Update URL, name, event_types, or is_active
DELETE /api/v1/webhooks/<id> Deactivate an endpoint
POST /api/v1/webhooks/<id>/rotate-secret Rotate the signing secret
POST /api/v1/webhooks/<id>/test Send a ping.test delivery
GET /api/v1/webhooks/<id>/deliveries List delivery attempts with status and response
POST /api/v1/webhooks/deliveries/<id>/redeliver Re-enqueue a failed 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.