Skip to content

Webhook payload

upgradescope serve --webhook <url> POSTs one JSON notification per cluster and evaluation pass that changed its readiness. When it is sent, how delivery is retried and how changes are grouped are in Running the server.

The payload has a JSON Schema, api/webhook.schema.json (draft 2020-12). It allows fields it does not list, because fields may be added within a schemaVersion; ignore the ones you do not know. A test delivers real notifications through ingest, the outbox and a signed request and validates them against it, so the schema and this page cannot drift from what the server sends. Another test regenerates the add-on changes of the example below from the embedded add-on registry at the example's timestamp, so their titles and details are what the engine prints.

Payload (schemaVersion 1)

POST with Content-Type: application/json:

{
  "schemaVersion": 1,
  "deliveryId": "3f2b8c1e9a7d4e6f8b0c2d4e6f8a0b1c",
  "type": "readiness.changed",
  "timestamp": "2026-10-02T09:30:00Z",
  "cluster": { "id": 7, "name": "prod-eu-1" },
  "targets": [
    { "target": "1.35", "verdict": "blocked", "score": 75, "blockers": 1 },
    { "target": "1.36", "verdict": "blocked", "score": 50, "blockers": 2 }
  ],
  "changes": [
    {
      "kind": "new-blocker",
      "key": "eol-addon/ingress-nginx",
      "severity": "blocker",
      "title": "Ingress NGINX Controller is end-of-life since 2026-03-24",
      "detail": "Detected Ingress NGINX Controller version 1.11.3 via image in namespace(s): ingress-nginx. Upstream support has ended.",
      "targets": ["1.35", "1.36"]
    },
    {
      "kind": "eol-approaching",
      "key": "eol-approaching/istio/1.30",
      "severity": "warning",
      "title": "Istio 1.30 reaches end-of-life on 2026-12-31",
      "detail": "Detected Istio version 1.30.1 via image in namespace(s): istio-system. Upstream support for the 1.30 release line ends on 2026-12-31.",
      "targets": ["1.36"]
    }
  ],
  "omitted": { "new-blocker": 3 }
}
Field Meaning
schemaVersion 1. Fields may be added within a version; a rename or removal bumps it.
deliveryId 32 hex characters, the same on every retry and for every sink.
type readiness.changed. The set of types is closed: a new one bumps schemaVersion (compatibility policy).
timestamp When the evaluation pass ran, RFC 3339 UTC.
cluster id (the API's cluster id) and name.
targets[] The verdict after the pass (ready, blocked or unknown), score and blocker count of every target with a change.
changes[] kind: new-blocker, became-ready or eol-approaching. key: the finding's stable key (none for became-ready); severity: blocker or warning; title, detail; targets: the targets the change applies to. Ordered new-blocker, became-ready, eol-approaching.
omitted Changes left out by the per-kind cap, by kind. Absent when nothing was left out.

Headers: X-Upgradescope-Delivery (the delivery id), X-Upgradescope-Event (the type) and, with a secret, X-Upgradescope-Signature.

Messages queued by a server older than this format are delivered after an upgrade as one-change notifications with a deliveryId of outbox-<n>.

Verifying the signature

serve --webhook-secret ($UPGRADESCOPE_WEBHOOK_SECRET, --webhook-secret-file; chart server.webhookSecret) signs every webhook request:

X-Upgradescope-Signature: sha256=<hex HMAC-SHA256(secret, raw request body)>

Compute the HMAC over the raw body bytes, before any JSON parsing, and compare in constant time:

mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
want := "sha256=" + hex.EncodeToString(mac.Sum(nil))
ok := hmac.Equal([]byte(want), []byte(r.Header.Get("X-Upgradescope-Signature")))
printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* /sha256=/'

The signature covers the body only; reject old timestamps and repeated deliveryIds to resist replay.