Skip to Content
Krad ADE
Agents API

App events and webhooks

The typed facts the app publishes about its own state, the per-group event journal on disk and webhooks signed with HMAC-SHA256.

Besides agent events, the app publishes app events: typed facts about its own state. They name ids, never contents, and go to the group's event journal, to the webhooks that chose them and to the extensions subscribed to them.

The record

One JSON object per event:

App event
{"api":1,"at":"2026-11-10T09:15:02.481Z","environment":"main","group":2,"event":{"kind":"agent_started","terminal":7,"harness":"ClaudeCode"}}
FieldMeaning
apiThe version of the event format: 1
atRFC 3339 in UTC with milliseconds
environmentThe environment's name
groupThe group's id; left out for events of no group
eventThe event, tagged by kind

Readers skip records of a newer api or an unknown kind.

Kinds

kindOther fieldsWhen
agent_startedterminal, harness (optional)An agent starts a turn
agent_waitingterminalAn agent waits for input
agent_finishedterminalAn agent finishes
worktree_createdfrom, the group it came fromA worktree group opens
worktree_removedA worktree is removed
session_openedsessionA session opens
session_closedsessionA session closes
git_changedThe group's git status changes
review_sentmerge_request, verdict (commented, replied, approved)A review is sent
ci_failed, ci_passedmerge_request, pipelineA pipeline of an open merge or pull request ends
merge_request_openedmerge_requestA merge or pull request opens
variants_startedrun, variants (how many)A variants run starts
variants_finishedrunEvery variant of a run is done
variant_winner_chosenrun, variant, outcomeA winner is picked
pingThe test event Settings sends

harness here is Shell, ClaudeCode, Codex, Opencode, Gateway or Acp:<id>.

The code host events are found by asking GitLab or GitHub every two minutes about the group's open merge and pull requests, up to 20 per project. The app never opens an address for them to call.

The event journal

Each group's events are appended, one record per line, to:

Config folder
events/<environment>/group-<id>.jsonl
events/<environment>/environment.jsonl     events of no group

A file that would pass 512 KiB rotates to .1.jsonl, .2.jsonl and .3.jsonl, the oldest dropped. The journal is read back skipping any line it cannot read. The config folder is described in Configuration.

Webhooks

Settings › Webhooks adds a webhook: a name, an address, the event kinds it wants and, optionally, a secret. The address uses https://; plain http:// is accepted only for this computer (localhost, 127.x.x.x or ::1), and an address may not carry credentials. The secret is kept in the system keyring, or in an owner-only file in the config folder without one.

For each event it chose, the app sends one POST with the record as the body:

HeaderValue
content-typeapplication/json
user-agentkrad-ade-webhooks
x-krad-timestampWhen this attempt was sent, RFC 3339 in UTC with milliseconds
x-krad-signature-256With a secret: sha256= and the lowercase hex HMAC-SHA256 of <timestamp>.<body>

Delivery and retries

AnswerWhat happens
2xxDelivered
4xx other than 408 and 429Given up at once
408, 429, 3xx, 5xx, or no answer within 10 secondsRetried, up to five attempts in all, waiting 2, 4, 8 and 16 seconds between them, never more than 60
  • A retry goes to the webhook as it is then: edited, it uses the new address; removed, it is dropped.
  • A webhook marked as signed whose secret is missing sends nothing.
  • The "Test event" button sends one ping, whatever kinds the webhook chose.

Verifying a signature

Read the raw body

Take the body exactly as received, before any JSON parsing, and the x-krad-timestamp and x-krad-signature-256 headers.

Recompute the HMAC

HMAC-SHA256 with the webhook's secret over the timestamp, a dot and the raw body, as lowercase hex after sha256=.

Compare and check the time

Compare in constant time, and turn away timestamps too far from now to stop replays.

verify.sh
expected="sha256=$(printf '%s.%s' "$timestamp" "$body" \
  | openssl dgst -sha256 -hmac "$KRAD_WEBHOOK_SECRET" -hex | sed 's/^.* //')"
[ "$expected" = "$signature" ] && echo valid

The shell comparison is not constant time; use it to test a receiver, not to guard one.

The five-minute window in the examples is a choice of the receiver; the app does not set one.

On this page