ampbase

OPAMP CONTROL PLANE

Documentation

Webhooks

Webhooks push channel events to an HTTPS endpoint you control — useful for piping deploy notifications into Slack, paging on agent-fleet anomalies, or feeding a downstream pipeline.

Ampbase delivers events as CloudEvents v1.0 in HTTP structured mode: the request body is a JSON document carrying every CE attribute inline, with Content-Type: application/cloudevents+json. You can consume Ampbase events with any off-the-shelf CloudEvents SDK.

Event types

Event When it fires
io.ampbase.agent.connected A new agent (by agent_id) completes its first OpAMP handshake.
io.ampbase.agent.disconnected An agent's heartbeat stops within the timeout window.
io.ampbase.config.deployed A configuration or bundle version becomes the active deployed version.
io.ampbase.config.rolledback The deployed version is changed to an earlier ULID.
io.ampbase.agent.limit.warning The channel's connected-agent count crosses the warning threshold for the plan.
io.ampbase.agent.limit.exceeded The channel's connected-agent count exceeds the plan limit.
io.ampbase.intelligence.finding.created The intelligence layer first observes a recommendation (a high-cardinality label, a cost hotspot). Also fires when a previously resolved finding regresses — the finding's state tells the two apart.
io.ampbase.intelligence.finding.resolved A re-analysis no longer reproduces a finding you hadn't dismissed — the thing it flagged is gone.

A webhook subscribes to one or more of these. You'll usually want io.ampbase.config.deployed plus io.ampbase.agent.limit.exceeded at minimum.

Intelligence findings are channel-scoped, not tied to a single agent, so their deliveries carry no subject. Dismissing a finding in the dashboard is an explicit "stop telling me about this" — it fires no webhook.

Finding payload

The data block for a finding event carries the derived analyzer discriminator plus the full finding as canonical protobuf JSON — the analyzer's report (cardinality, cost, …) alongside its evidence packet: the provenance of the measurement (window, calculation method and whether it's approximate, coverage, and the query reference needed to re-run it).

POST https://your-endpoint.example.com/ HTTP/1.1
Content-Type: application/cloudevents+json
X-Ampbase-Timestamp: 1700000200
X-Ampbase-Signature: sha256=...

{
  "specversion": "1.0",
  "id": "01HXK...",
  "source": "https://ampbase.io/orgs/01HQR…/channels/01HXY…",
  "type": "io.ampbase.intelligence.finding.created",
  "time": "2026-07-19T10:30:00Z",
  "datacontenttype": "application/json",
  "data": {
    "analyzer": "cardinality",
    "finding": {
      "fingerprint": "9f2c…",
      "org_id": "01HQR…",
      "channel_id": "01HXY…",
      "source": { "kind": "SOURCE_KIND_RULE_BASED" },
      "state": "FINDING_STATE_OPEN",
      "first_seen": "2026-07-19T10:30:00Z",
      "last_seen": "2026-07-19T10:30:00Z",
      "cardinality": {
        "channel_id": "01HXY…",
        "metric_name": "http_requests_total",
        "attribute_key": "user_id",
        "cardinality": "50000"
      },
      "evidence": {
        "as_of": "2026-07-19T10:30:00Z",
        "window": { "start": "2026-07-12T10:30:00Z", "end": "2026-07-19T10:30:00Z" },
        "calculation": { "method": "HLL++ sketch merge estimate", "approximate": true },
        "coverage": { "rows_observed": "20", "truncated": true },
        "query": { "analyzer_version": "cardinality/v1", "params": { "window_days": "7" } }
      }
    }
  }
}

Route on analyzer to pick a handler, and on the finding's state to distinguish a brand-new FINDING_STATE_OPEN from a FINDING_STATE_REGRESSED re-occurrence. Enum fields use their protobuf constant names; numbers arrive as JSON strings, per protobuf-JSON convention.

Setup

  1. Webhooks → New webhook.
  2. Enter your HTTPS endpoint URL and select event types.
  3. Save. The webhook starts in PENDING_VERIFICATION state.

Endpoint verification

Before any real event fires, Ampbase needs proof that you control the URL. It POSTs a one-shot verification challenge using the same CloudEvents envelope as real deliveries — so your endpoint only needs one Content-Type parser:

POST https://your-endpoint.example.com/ HTTP/1.1
Content-Type: application/cloudevents+json
X-Ampbase-Event: io.ampbase.webhook.verification
X-Ampbase-Webhook-ID: wh_01HXJ...
X-Ampbase-Verification-Challenge: 8f2c4a7e9b1d3a5f7e2c4a7e9b1d3a5f
X-Ampbase-Timestamp: 1700000000
X-Ampbase-Signature: sha256=...

{
  "specversion": "1.0",
  "id": "01HXJ...",
  "source": "https://ampbase.io/webhooks/wh_01HXJ...",
  "type": "io.ampbase.webhook.verification",
  "subject": "webhook/wh_01HXJ...",
  "time": "2026-05-18T10:30:00Z",
  "datacontenttype": "application/json",
  "data": {
    "webhook_id": "wh_01HXJ...",
    "challenge": "8f2c4a7e9b1d3a5f7e2c4a7e9b1d3a5f"
  }
}

X-Ampbase-Event, X-Ampbase-Webhook-ID, and X-Ampbase-Verification-Challenge are convenience headers for this one POST so endpoints can echo the challenge without parsing the body. They are only set on the verification handshake — never on regular deliveries — and like all headers they're outside the HMAC, so the signed body remains the authoritative source.

Your endpoint must reply with HTTP 200 and the challenge value somewhere in the response body — echoing it back as JSON ({"challenge":"…"}) is fine, as is returning it as a plain string. Once verified, the webhook flips to ACTIVE and starts receiving real events. Re-run verification any time from the UI.

Delivery and HMAC signing

Each event delivery POSTs a CloudEvent and includes two signature headers:

POST https://your-endpoint.example.com/ HTTP/1.1
Content-Type: application/cloudevents+json
X-Ampbase-Timestamp: 1700000123
X-Ampbase-Signature: sha256=a1b2c3...

{
  "specversion": "1.0",
  "id": "01HXJ...",
  "source": "https://ampbase.io/orgs/01HQR…/channels/01HXY…",
  "type": "io.ampbase.config.deployed",
  "subject": "agent/01HVZ8...",
  "time": "2026-05-18T10:30:00Z",
  "datacontenttype": "application/json",
  "data": { "config_id": "01ARZ3NDFV", "version_ulid": "01ARZ3...", "agent_id": "01HVZ8..." }
}

The signature is HMAC-SHA256(secret, timestamp + "." + raw_body) hex-encoded, prefixed with sha256=. Binding the timestamp into the MAC prevents replay of an old delivery with a fresh X-Ampbase-Timestamp. The secret itself is prefixed whsec_ and is shown once on webhook creation — store it like an API key.

Verify on your side before trusting any field of the payload — including the event type:

import crypto from "crypto";

function verify(req) {
  const ts        = req.headers["x-ampbase-timestamp"];
  const signature = req.headers["x-ampbase-signature"]; // "sha256=…"
  if (!ts || !signature) return false;

  // Reject anything more than 5 minutes off — replay window.
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const expected = "sha256=" + crypto
    .createHmac("sha256", process.env.AMPBASE_WEBHOOK_SECRET)
    .update(ts + "." + req.rawBody)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature),
  );
}

After verify() returns true, parse the body and route on the CloudEvents type field — it's the only field guaranteed to reflect what Ampbase actually sent:

const event = JSON.parse(req.rawBody); // already verified above
switch (event.type) {
  case "io.ampbase.config.deployed":
    /* … */ break;
  case "io.ampbase.agent.limit.exceeded":
    /* … */ break;
}

Retries and status lifecycle

Deliveries that don't return 2xx are retried with exponential backoff. After repeated failures the webhook progresses through degraded states:

ACTIVE  →  DEGRADED   (one or more recent failures, still retrying)
        →  DISABLED   (10 consecutive failures — no more deliveries until you re-verify)

A disabled webhook stays in the UI with the reason recorded. Fix the endpoint, re-run verification, and it returns to ACTIVE.

Because retries can redeliver an event, every CloudEvent carries a unique id you can dedupe on. One case needs a different key: for io.ampbase.intelligence.finding.created, two analysis runs racing the same brand-new finding can each emit it with a different id. Those duplicates share the same finding.fingerprint and finding.first_seen, so dedupe findings on that pair.

Operational tips

  • Respond fast (under 5 seconds) — long-running side effects should be deferred to a worker on your side.
  • Be idempotent: retries mean the same event may arrive more than once.
  • Always verify the signature before reading the body. Don't route on request headers — they're not covered by the HMAC. The signed type attribute inside the body is authoritative.

Spotted a problem with these docs? Email support@ampbase.io.