Skip to main content

Custom Alert Source

The Custom Alert Source lets any system that can send an HTTP POST deliver alerts into Autoheal — without waiting for a purpose-built integration. You point your system at a private webhook URL, send your own JSON payload, and describe once how your fields map to an Autoheal alert. From there the alert flows through the normal pipeline: grouping, triage, auto-investigation, alert-response routing, and notifications.

Use this for homegrown monitoring, security tools (e.g. GCP Security Command Center), or any source without a first-class Autoheal integration.

How it works

  1. Create a Custom Alert Source integration. You immediately get a unique Webhook URL and (optionally) set a Bearer Secret.
  2. Configure your system to POST its native alert JSON to that URL.
  3. Fill in the Payload Mapping — dotted field-paths that tell Autoheal which parts of your payload are the title, severity, dedup identity, resolve status, and any labels.
  4. Use Preview to paste a sample payload and see the exact alert it produces before going live.

You send your payload as-is — Autoheal adapts to your shape, not the other way around.

Endpoint & authentication

POST https://<your-autoheal-host>/api/v1/webhooks/customsource/<secret>
Content-Type: application/json

Use the exact Webhook URL shown on the integration's Configure page — it already includes the correct host and secret.

  • URL secret (always on): the <secret> in the path is unguessable and unique to your integration. Treat the full URL as a credential.
  • Bearer secret (optional, recommended): set a Bearer Secret on the integration and send it as a header. When set, requests without a matching header are rejected.
Authorization: Bearer <your-bearer-secret>

Payloads are limited to 1 MB. HMAC request signing is not required.

Payload mapping

Each mapping field is a dotted JSON path into your payload (e.g. finding.severity, alerts.0.host). Array elements are addressed by index.

FieldRequiredPurpose
IdentityRecommendedA value unique and stable per alert (e.g. a finding id, host+check). Redeliveries with the same identity collapse into one alert; a later resolved event with the same identity closes it. If omitted, Autoheal fingerprints the payload contents — so exact redeliveries still collapse — but a later resolved event won't match, so the alert won't auto-resolve. Map an Identity to enable resolve-tracking.
TitleRecommendedThe human-readable alert title. If omitted, Autoheal auto-picks a common field (title, summary, message, name, …), then falls back to the identity, then "Custom alert".
Alert NameOptionalA transition-independent name for the underlying rule. Defaults to the title. Produces the alertname label.
SeverityOptionalNormalized to critical / high / medium / low (common words recognized automatically; use a value map for custom vocab like SEV-1 → critical; defaults to medium). The platform publishes this as the canonical severity label automatically — so don't also add a severity label.
StatusRecommendedThe path to your state field, plus the Resolved Values that mean "closed" (e.g. INACTIVE, RESOLVED, OK). Without this, the source never auto-closes.
Resolved AtOptionalPath to a resolve timestamp. If omitted, the receipt time is used when a resolve arrives.
Event TimeOptional (recommended for grouped)Path to the event's own firing timestamp (RFC 3339 or Unix epoch). Sets the alert's start time. In grouped mode this is what lets a silenced alert still persist (an alert with no source timestamp relies on the grouped callback, which a silence suppresses). Must be a stable per-event value — the platform never synthesizes one from "now", which would fragment grouping on redelivery.
LabelsOptionalName → path projections. Labels are values used for grouping, routing, and dedup (e.g. service, host, region). Or set "capture all fields under a path as labels" to promote an entire object (see Catch-all below).
AnnotationsOptionalName → path projections for context shown on the alert (descriptions, ids, URLs). Or set "capture all fields under a path as annotations" to promote an entire object.

Notes:

  • Unmapped fields are captured automatically. Any top-level field you don't explicitly map is added as an annotation, so your data shows up with minimal configuration. Only top-level scalar values are auto-captured — nested objects are not (map the nested fields you care about with dotted paths, or turn on Retain raw payload).
  • Retain raw payload (Advanced) stores the entire original JSON — nested fields and all — on the alert for viewing on the alert detail. It's opt-in because the raw body may contain sensitive data.
  • You can send with no mapping at all. The alert still appears — titled from a common field (title, summary, message, name, …) or "Custom alert", with every top-level field as an annotation. Add mapping to control title/severity, enable identity-based dedup and auto-resolve (without it, only byte-identical redeliveries collapse), and promote fields to labels.
  • Catch-all promotion. If your source already emits a curated object of labels or tags (e.g. {"labels": {"service": "checkout", "region": "us-east"}}), you don't need one row per key: set "capture all fields under a path as labels" to that object's path (labels). Its direct scalar children each become a label. The annotations catch-all works the same way. Guardrails apply automatically: keys are sanitized to the label grammar, non-scalars and nested objects are skipped, explicit rows win, reserved names (severity/alertname/fingerprint) are skipped for labels, and the total stays bounded.
  • Grouping is a separate layer. Extra labels don't change grouping — only the labels named in your Default Grouping (group_by) settings do. So promoting many labels is safe; they're just available for routing/matchers/filtering. Label names must be letters, digits, and underscores (not starting with a digit).
  • Optional transforms (a bounded regular-expression replace) and defaults are available per field for light reshaping.

Title vs Alert name

These two look similar but do different jobs, and the difference matters for grouping and dedup:

  • Title is the human-readable text shown in the UI. It can change from event to event — "Disk 92% on db-1" then "Disk 95% on db-1".
  • Alert name is the stable identity of the underlying rule. It becomes the alertname label, which is part of the alert's dedup/grouping identity, so it must not change between events or across the firing → resolved transition.

If your title is already stable, leave Alert name empty — it falls back to the title. Set it separately only when your title embeds a changing value:

TitleAlert name
ValueCPU at 93% (varies each event)HighCPU (constant)
Roledisplayidentity → alertname label

Using the varying title as the identity would give every event a different alertname, so they'd never group or dedupe into one alert.

Reserved fields — use the dedicated field, not a label

A few label names are set by the platform itself, so they can't be used as custom label keys. Map them to their dedicated field instead — you get more than a raw label would, and the canonical label is still published automatically (so group_by severity still works):

Reserved labelSet it viaWhat you get
severitySeverity fieldnormalized critical/high/medium/low, the severity badge + routing, and the severity label
alertnameTitle / Alert Namethe alert's name, and the alertname label
fingerprintIdentitythe dedup / resolve key
autoheal_*reserved namespace, never customer-writable

Adding one of these as a label is rejected at save/preview with a message pointing you to the right field.

Grouping

The Enable grouping toggle (in Delivery) is off by default:

  • Off (default): each event is its own alert group — ideal for one-shot findings and heartbeat failures. Redeliveries collapse; a resolve closes the group in place.

  • On: events route through Alertmanager so related ones cluster together. Grouping uses your Default Grouping (group_by) rules (Alert Response → Grouping) — there is no per-source group_by, so promote the fields you want to cluster on (e.g. service, region) to labels and add them to your Default Grouping settings.

    Map an Event Time (Advanced) when using grouped mode. Without a source timestamp, a grouped alert relies on Alertmanager's grouped callback to be persisted — so if the alert matches a silence, it leaves no record. Mapping a stable Event Time sets the alert's start time and persists it up front, the same way first-class integrations do.

Batching

One request, many alerts. By default each POST becomes one alert. If your system sends an array of alerts in a single request, set Batch — array of events (Advanced) to the JSON path of that array. Autoheal then runs the mapping once per element.

Key rule: once a batch selector is set, every mapping path is relative to each array element, not the top of the payload.

For this payload:

{
"alerts": [
{ "id": "cpu-db1", "summary": "CPU 93% on db-1", "severity": "high", "host": "db-1" },
{ "id": "disk-db2", "summary": "Disk 88% on db-2", "severity": "warning", "host": "db-2" }
]
}

set Batch — array of events = alerts, then map the fields as they appear inside an element:

{
"array_selector": "alerts",
"identity": { "path": "id" }, // "id" — NOT "alerts.id"
"title": { "path": "summary" },
"severity": { "path": "severity" },
"labels": [ { "key": "host", "path": "host" } ]
}

→ two separate alerts (cpu-db1 and disk-db2), each with its own identity, dedup, and resolve tracking.

A common mistake is writing alerts.id instead of id — that path doesn't exist inside an element, so it matches nothing and the label is dropped. Preview flags exactly this, per element:

label.id [#1]: path "alerts.id" matched nothing in this element (paths are relative to each array element when a batch array selector is set)

Without a batch selector, that same payload would produce one alert whose alerts field is a nested array (not auto-captured) — you'd only reach the first element with an indexed path like alerts.0.id. Batching is what turns "N events in one webhook" into "N alerts."

Configuration by example

Say you run a homegrown monitoring service, ObsOne. Here's how the same source grows from zero config to a full mapping.

1. Zero config

Point ObsOne at your webhook URL and send anything. Autoheal titles the alert from a common field and captures every top-level field as an annotation — no mapping required.

{ "summary": "disk 92% on db-1", "host": "db-1", "env": "prod", "severity": "high" }

→ Title disk 92% on db-1, severity high, and host/env as annotations. Good for a first look; no dedup or auto-resolve yet.

2. Basic mapping

Add an Identity (for dedup + auto-resolve) and promote the fields you route on to labels. For this payload:

{ "id": "disk-db1", "summary": "disk 92% on db-1", "severity": "high", "host": "db-1", "env": "prod" }

use:

{
"identity": { "path": "id" },
"title": { "path": "summary" },
"severity": { "path": "severity" },
"labels": [ { "key": "host", "path": "host" }, { "key": "env", "path": "env" } ]
}

→ one alert titled disk 92% on db-1 (severity high), keyed on disk-db1 so re-sends collapse into it, with host and env as labels.

3. Auto-resolve + grouping

Tell Autoheal how ObsOne signals recovery, and turn on Enable grouping so related events cluster (by your Default Grouping rules):

{
"identity": { "path": "id" },
"title": { "path": "summary" },
"severity": { "path": "severity" },
"status": { "path": "state", "resolved_values": ["ok", "resolved"], "resolved_at_path": "resolvedAt" },
"labels": [ { "key": "service", "path": "service" } ],
"ingest_mode": "grouped"
}

4. Nested fields and catch-all

ObsOne nests its context. Reach nested fields with dotted paths, and use the catch-all to promote a whole labels/details object without listing every key:

{
"identity": { "path": "alert.id" },
"title": { "path": "alert.summary" },
"severity": { "path": "alert.severity" },
"labels_from": "alert.labels", // every scalar under alert.labels → a label
"annotations_from": "alert.details" // every scalar under alert.details → an annotation
}

For the payload {"alert":{"id":"a1","summary":"…","labels":{"service":"checkout","region":"us-east"},"details":{"runbook":"…","trace_id":"…"}}}, service and region become labels and runbook/trace_id become annotations — no per-key rows.

5. Batches

If ObsOne posts several alerts in one request, set Batch — array of events to the array's path (e.g. alerts); each element becomes its own alert. See Batching for the (important) element-relative path rule.

Worked example — GCP Security Command Center

SCC publishes findings to Pub/Sub. Point a Pub/Sub HTTP push subscription at your webhook URL and map the finding:

{
"identity": { "path": "finding.name" },
"title": { "path": "finding.category" },
"severity": { "path": "finding.severity", "value_map": { "HIGH": "high", "CRITICAL": "critical" } },
"status": { "path": "finding.state", "resolved_values": ["INACTIVE"], "resolved_at_path": "finding.eventTime" },
"labels": [
{ "key": "resource", "path": "finding.resourceName" },
{ "key": "category", "path": "finding.category" }
],
"annotations": [
{ "key": "description", "path": "finding.description" }
]
}

A High/Critical finding now arrives as an Autoheal alert, dedupes on re-push, closes when SCC sets state = INACTIVE, and routes to your configured Slack channel.

Testing it

curl -X POST "https://<your-autoheal-host>/api/v1/webhooks/customsource/<secret>" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <your-bearer-secret>" \
-d '{"finding":{"name":"finding-123","category":"MALWARE","severity":"HIGH","state":"ACTIVE","resourceName":"//compute/vm-1","description":"suspicious binary"}}'

A 200 {"status":"accepted"} means the payload was received. Check the Alerts page for the resulting alert, and the integration's event history for delivery status.