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
- Create a Custom Alert Source integration. You immediately get a unique Webhook URL and (optionally) set a Bearer Secret.
- Configure your system to POST its native alert JSON to that URL.
- 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.
- 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.
| Field | Required | Purpose |
|---|---|---|
| Identity | Recommended | A 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. |
| Title | Recommended | The 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 Name | Optional | A transition-independent name for the underlying rule. Defaults to the title. Produces the alertname label. |
| Severity | Optional | Normalized 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. |
| Status | Recommended | The 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 At | Optional | Path to a resolve timestamp. If omitted, the receipt time is used when a resolve arrives. |
| Event Time | Optional (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. |
| Labels | Optional | Name → 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). |
| Annotations | Optional | Name → 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
alertnamelabel, 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:
| Title | Alert name | |
|---|---|---|
| Value | CPU at 93% (varies each event) | HighCPU (constant) |
| Role | display | identity → 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 label | Set it via | What you get |
|---|---|---|
severity | Severity field | normalized critical/high/medium/low, the severity badge + routing, and the severity label |
alertname | Title / Alert Name | the alert's name, and the alertname label |
fingerprint | Identity | the 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-sourcegroup_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.