Skip to content

Webhooks

Webhooks turn the event stream into an outbound push: kasas POSTs each subscribed event to an HTTP endpoint you register, HMAC-signed, so external apps react to changes without polling. This is what makes kasas an integration hub — budgeting, accounting, tax, fraud detection, and notification apps build on the events without kasas implementing any of them.

Source: internal/webhooks. Requires events.enabled and webhooks.enabled (both default on).

How delivery works

The dispatcher rides the event bus and hands matching events to a small worker pool that signs and delivers them, with retries and persisted health.

sequenceDiagram
    autonumber
    participant Bus as Event bus
    participant D as Dispatcher
    participant DB as Store
    participant W as Worker pool
    participant EP as Your endpoint

    Bus->>D: event committed
    D->>DB: load enabled webhooks
    loop each webhook matching the event type
        D->>W: enqueue (non-blocking — drop if queue full)
    end
    loop attempts 1..max_attempts
        W->>EP: POST event + HMAC signature
        alt 2xx
            EP-->>W: ok → stop
        else failure
            EP-->>W: error / timeout → backoff & retry
        end
    end
    W->>DB: record last_status / last_error / last_success_at

Defaults: an 8-worker pool, a 256-deep job queue, a per-attempt timeout of webhooks.timeout (10s), up to webhooks.max_attempts (5) tries with exponential backoff (500 ms, doubling, capped at 30s). If the queue is full the bus reader drops rather than blocks — and the gap is reconciled by replay (below).

Registering a webhook

Subscribe to specific event types (the same taxonomy as the stream) or to all of them with *:

curl -X POST -H "Authorization: Bearer $KASAS_DASHBOARD_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://my-app.example.com/hooks/kasas","event_types":["transaction.created","transaction.updated"]}' \
  http://localhost:8080/api/v1/webhooks
# -> {"id":1,"url":…,"event_types":[…],"enabled":true,"secret":"whsec_…"}

The signing secret is returned once, at creation. An empty event_types (or ["*"]) subscribes to everything.

The delivery request

Each delivery is a JSON POST of the event — the same envelope as the REST/SSE event — with these headers:

X-Kasas-Event:     transaction.created
X-Kasas-Event-Id:  <uuid>             # idempotency / dedupe key
X-Kasas-Timestamp: <unix seconds>
X-Kasas-Signature: sha256=<hex HMAC>

Verifying the signature

Recompute HMAC-SHA256(secret, "<timestamp>.<body>") over the raw request body and compare in constant time. Reject stale timestamps to thwart replay:

expected = "sha256=" + hmac.new(
    secret.encode(), f"{ts}.{raw_body}".encode(), hashlib.sha256
).hexdigest()
assert hmac.compare_digest(expected, request.headers["X-Kasas-Signature"])

Always dedupe on X-Kasas-Event-Id, since retries and replays can re-deliver an event.

Reliability: best-effort + replay

Delivery is best-effort, not a durable queue. kasas retries with backoff and records each endpoint's last-delivery health, but if an endpoint is down long enough, or kasas restarts, some deliveries are missed.

flowchart LR
    NORMAL[live: bus → deliver] -->|burst, large sync,<br/>restart, slow endpoint| DROP[subscription dropped]
    DROP --> RESUB[dispatcher resubscribes]
    RESUB --> REPLAY["replay durable log<br/>GET /events?after=lastSeq"]
    REPLAY --> NORMAL

This is the same drop-and-replay that all event consumers use — and it is the normal path, not just an error path: a large sync can momentarily outrun the bus buffer, so the dispatcher routinely resubscribes and replays the gap from the durable event log. Your endpoint catches up the same way any consumer does. That durable log is exactly why missed pushes are recoverable — if you'd rather pull, reconcile yourself via GET /api/v1/events?after=<sequence>.

Managing webhooks

Surface Operations
REST GET/POST/PUT/DELETE /api/v1/webhooks, POST /{id}/test, POST /{id}/rotate-secret
MCP list_webhooks, create_webhook, update_webhook, delete_webhook, test_webhook
Dashboard The Webhooks page: register, edit, toggle, send a test delivery, and see per-endpoint health

Registering and managing webhooks is admin-only (the dashboard token, never an API key). Delivery is metered with kasas_webhook_deliveries_total, kasas_webhook_delivery_attempts_total, and kasas_webhook_deliveries_dropped_total.

Webhooks vs. plugins

Webhooks push events to a separate service over HTTP. If you'd rather run your reaction in-process, in a sandboxed VM with direct (capability-gated) access to the ledger, that's what plugins are — the same events, consumed on the inside.