Webhook reference

Every inbound webhook adapter — URL forms, the fields each one reads, how service, severity, status and identity are derived, the response counters, correlation rules and the exact error strings.

Reference · Updated 2026-10-27 · Describes release 0.1.164

Exact facts about the inbound webhook plane, one section per adapter. For the walkthroughs that set a sender up, read Alerts from your observability platform and Alert sources and intake rules; this page is what those pages point at when a field, a mapping or an error string matters.

URL forms and authentication

POST https://<your-host>/webhook/<source>/<workspace-slug>
POST https://<your-host>/webhook/<source>/<workspace-slug>/<token>

<source> is one of the adapter names below. alertmanager, generic and change are their own routes; every other source is served by one shared adapter route, except kubernetes-audit, whose only path form ends in the cluster name and takes the cluster's own audit token.

The credential is the workspace's webhook token, read in this order; the first one present wins:

Form Header or path Note
Bearer header (preferred) Authorization: Bearer <token> What Alertmanager's http_config.authorization and Robusta's sink authorization produce. Headers never land in access or proxy logs.
Token header X-Webhook-Token: <token> For senders without bearer support.
Path segment /webhook/<source>/<workspace-slug>/<token> Kept for compatibility and demonstrations; the only form an Amazon SNS subscription or an Azure Monitor action group can send. Lands in access logs.

Only the SHA-256 hash of the token is stored. A bad slug, a disabled workspace, a workspace that has never minted a token and a wrong token all answer the same 401 invalid webhook credentials, so nothing about the workspace leaks to whoever posts at it. Minting a new token (Alert intake & tokens → Webhook ingest → Mint webhook token, pillars.manage) invalidates the previous one immediately; there is no overlap window.

A token that travels in one of the platform's own intake URLs — the path segment after the slug, or a token= query value — is redacted as [REDACTED:secret] before an alert, a change or a finding is stored.

Rate limit

A per-workspace token bucket over alert items (or change events), not HTTP requests: one payload may carry a whole storm. The limit is aiops_ingest_rate_per_min (platform default 1200 items per minute, one minute of burst; a workspace value of 0 or less switches it off). When the bucket is dry the whole payload is refused with 429 and a Retry-After header in seconds, so a sender that honours Retry-After redelivers everything and nothing is silently dropped. The refusal is audited as ingest.rate_limited at most once a minute per workspace.

Response body

Every alert endpoint answers the same counter set, whatever the source. The two scanner-specific counters are present as 0 for ordinary alert sources, so never assert on an exact key set.

Field Meaning
received Alert items the adapter produced from the payload. 0 with a 200 means the adapter recognised none of its fields.
alerts_created New alert rows. alerts_created ≈ received together with incidents_created growing 1:1 is the signature of failed attribution: every alert is opening its own incident.
deduped Items whose fingerprint is already active on an open incident. deduped > 0 on a repeat delivery proves the identity is stable.
suppressed Items that joined an existing incident through their grouping label or a fold. Large during a burst on one service. Tickets are counted under joined instead.
joined Tickets (ServiceNow, Jira, Azure DevOps) that joined an existing incident — a ticket about a service the platform is already alerting on. The ticket is kept as evidence on that incident, never dropped.
risk_suppressed Scanner findings held down by a live risk acceptance (Security Hub / Defender only).
scoped_out Scanner findings outside the workspace tag scope (workspace_scope, platform-staff setting).
stale_resends Security Hub / Defender re-sends observed before the recorded fix: counted on the register row, no incident opened.
ticket_echo Tickets the platform itself filed, coming back through a ticket webhook — the create, every update and the resolve, whoever resolved it: counted, never a second incident.
alerts_resolved Alert rows a resolved item flipped. Proves the recovery matched the firing identity — but only the alert row.
incidents_created Ids of the incidents this delivery opened.
incidents_resolved Ids of the incidents this delivery closed. Stays empty while any other alert on the incident still fires, and while a person holds the row (escalated, awaiting_approval, remediating) — that case records the clear and leaves the close to the person. The counter to assert on when testing recovery wiring.
investigations Ids of the analysis runs this delivery started.

Change-carrying deliveries answer {"recorded": N, "flagged_unauthorized": M} instead — no alert counters (see Change sources). A source that has a change extractor but no alert adapter answers {"recorded": 0} when the payload yields no change event. An SNS SubscriptionConfirmation answers {"confirmed": true|false}.

Canonical labels and workspace maps

Every adapter produces alert items of one shape: fingerprint, status (firing or resolved), labels, annotations, starts_at and the raw payload. The canonical labels every later stage reads:

Label Role
service The correlation identity. Alerts sharing it join one incident; blast radius, topology folds and the suspect-change pin key on it.
alertname The rule name; the grouping key when no service (and no namespace) is present.
severity critical, error, warning or info. Everything else ranks below info.
namespace Second grouping key, after service.
resource, account, region, subscription, resource_group, resource_type, cluster, env Carried for the agent, the incident list filters and the tier rules; never grouping keys.
criticality Stamped from the alert's own criticality / business_criticality label or the workspace criticality_map (low, medium, high, mission-critical); raises priority and the risk tier, never changes grouping.

Four workspace settings rewrite labels before deduplication, in this order:

  1. label_map {"source_key": "canonical_key"} renames label keys. The hook that makes a source with its own vocabulary correlate with everything else (for example {"FunctionName": "service"}).
  2. severity_map {"source_value": "canonical"} rewrites the severity value when it matches exactly.
  3. source_service_labels {"<source>": ["<label>", …]} (what the Unidentified service — map this source card saves) names the service of an alert from that source that still carries no service: the first listed label the alert carries is copied onto service; an alert's own service is never replaced, and no other source is touched.
  4. service_alias {"<alert service>": "<dependency-map service>"} (the Service aliases card) is the last step: an alert whose service is listed takes the dependency map's name, so it keys, folds and reads its blast radius there; the original stays as service_original. One hop, never a chain (see Service aliases).

All four are workspace settings set with PATCH /api/aiops/{tenant_id} (members.manage); source_service_labels also has its card under Alert intake & tokens → Intake rules → Mapped sources, and service_alias its Service aliases card below it. The Settings reference lists their shapes and checks.

Identity, grouping and storms

Dedupe is by fingerprint: an item whose fingerprint is active on an open incident is counted as deduped and dropped, whatever its labels say. Every adapter below states what its fingerprint is made of.

Grouping key is the strongest of service=<v>, namespace=<v>, alertname=<v> the item carries, in that order, within one pillar; an item with none of them keys on fingerprint=<fp> and never groups. There is no time window: an alert joins the open incident with its key regardless of age. The first alert of an incident is correlated, later ones suppressed.

Pillar routing: severity: security routes to SecOps; a finding_family label routes threat-detected to SecOps and every other family to Compliance; a source, labels.source or labels.tool value of guardduty, securityhub, falco, trivy, wazuh or crowdstrike (case-insensitive) routes to SecOps; everything else is Incident and Problem. The pillar is part of the grouping bucket, so a Falco alert for payments-api never joins the telemetry incident for the same service.

Grouping by the service graph: an alert that would open (or reopen) an incident for a service whose dependents already have open incidents (opened or reopened inside aiops_storm_group_window_s + aiops_storm_spread_window_s, 900 s) opens it and takes theirs in, over edges allowed to fold (a caller's analysis waits up to aiops_storm_group_window_s for its dependency's alert, unless it is a P1); with aiops_storm_agent on (the default) one bounded turn later groups related incidents over every recorded edge. A burst of unrelated services opens one incident each (an alert name already firing on 3 services still joins their incident, aiops_fold_shared_cause_threshold); there is no storm threshold and no __storm__ incident since 0.1.147. The rules and the review steps are in Alert storm: grouped by the service graph.

Holds at ingest: an alert naming no service opens its own held unmapped=<fp> incident when aiops_unmapped_triage is on; so does a ServiceNow, Jira or Azure DevOps ticket that does not name every field in ticket_required_fields (the platform asks on it once, see Tickets that come in must name five fields); an alert with neither a service-ish label nor an alertname is held when aiops_require_identity_labels is on; a built-in detection warning rule opens its incident with no run. Held incidents wait in the triage queue for Analyse.

Reopen: when aiops_reopen_window_h is above 0, an alert whose key matches an incident resolved inside the window reopens that incident instead of opening a new one; aiops_flap_threshold reopens inside the window mark it flapping. Storm and fingerprint keys never reopen, and neither does an incident a person closed as known noise.

Priority is derived at open and re-derived on every change of severity, blast radius or criticality: critical → P1, error → P2, warning → P3, anything else → P4; one level up (floor P1) when the computed blast radius reaches two services (failing dependents grouped in count since 0.1.147), and one more for a mission-critical resource. The owner a new incident opens with comes from assignment_map {"<service glob>": "<assignee>"}: the first pattern matching the alert's service (its namespace when there is no service).

Alertmanager

POST /webhook/alertmanager/<workspace-slug> — the standard Alertmanager webhook body, the highest-fidelity path: labels and annotations pass through verbatim.

Payload field Canonical field
alerts[].fingerprint, else a hash of the labels fingerprint
alerts[].status, else the top-level status, else firing status
alerts[].labels labels (verbatim)
alerts[].annotations annotations (verbatim)
alerts[].startsAt starts_at

Configure the receiver with send_resolved: true and the token through http_config.authorization (type Bearer). A payload with no alerts list answers 200 with "received": 0.

Generic

POST /webhook/generic/<workspace-slug> — {source, alerts: [{fingerprint, status, labels, annotations, message, startsAt}]}. You control every canonical field.

Payload field Canonical field Note
source (top level) the recorded alert source and a pillar selector Defaults to generic. The reserved value builtin is rewritten to generic (it names the platform's own detector). The security-tool names listed under Identity, grouping and storms send the alert to SecOps.
alerts[].fingerprint fingerprint If omitted: a hash of labels, or of annotations when there are no labels. Send your own — a slug of <service>:<condition> is ideal.
alerts[].status status Defaults to firing. Send the recovery as the same fingerprint with "status": "resolved".
alerts[].labels labels Verbatim.
alerts[].annotations annotations Verbatim.
alerts[].message annotations.message Convenience, when annotations.message is not already set.
alerts[].startsAt or alerts[].starts_at starts_at ISO-8601 or epoch seconds/milliseconds. The onset the suspect-change pin looks back from; without it the receipt time is used.

CloudWatch via SNS

POST /webhook/cloudwatch-sns/<workspace-slug>[/<token>] — the SNS notification envelope whose Message is the alarm JSON as a string; with RawMessageDelivery the alarm JSON may be the whole body. SNS cannot set a header, so the token rides in the path. The SubscriptionConfirmation handshake is answered automatically on any /webhook/<source> path; only an https://sns.<region>.amazonaws.com SubscribeURL is ever fetched.

Alarm field Canonical field Note
AlarmArn, else AlarmName fingerprint Trigger and recovery share it.
AlarmName labels.alertname Required; a payload without it yields no alert.
Trigger.Dimensions[].{name,value} (or Name/Value) labels.<name> = <value> Every dimension becomes a label, verbatim.
Trigger.Metrics[] (anomaly-detection and metric-math alarms) the dimension rows above The metric the ThresholdMetricId band is computed over wins; else the first MetricStat with dimensions; else the first.
service=<name> in AlarmDescription, else dimension ServiceName or service, else the most specific resource-identifier dimension labels.service Identifier order: InstanceId, VolumeId, DBInstanceIdentifier, DBClusterIdentifier, CacheClusterId, ClusterIdentifier, ClusterName, FunctionName, StateMachineArn, TableName, QueueName, TopicName, StreamName, BucketName, FileSystemId, TargetGroup, LoadBalancer, ApiName, DistributionId, ProjectName, PipelineName, JobQueue, DomainName, BrokerName, WorkGroup, DeliveryStreamName, then AutoScalingGroupName. An Application Signals alarm with no dimensions takes the eks:<cluster> of its Environment expression. Category-only dimensions (StorageType, Currency) give no service: with the triage queue on (the default since 0.1.133) the alarm opens its own held unmapped=<fingerprint> incident; with it off it groups by alertname.
most specific identifier, else the first dimension value labels.resource Only when resource is not already set.
Trigger.Namespace labels.metric_namespace Deliberately not namespace: AWS/EC2 is a metric taxonomy, not a failure domain.
<Namespace>/<MetricName> annotations.metric Which metric fired.
AWSAccountId labels.account
AlarmArn field 4 labels.region
dimension InstanceId with AWSAccountId and the AlarmArn region annotations.resource_arn arn:aws:ec2:<region>:<account>:instance/<id>, what a step runbook binds with from: annotation:resource_arn; not set when the alarm's metric belongs to another account.
NewStateValue status OK → resolved; ALARM and INSUFFICIENT_DATA → firing.
AlarmDescription, else the name annotations.summary Becomes the incident title.
NewStateReason annotations.message
StateChangeTime starts_at
severity=<level> in AlarmDescription, else critical labels.severity high, warn and low read as critical, warning and info; a dimension literally named severity overwrites both.

Point the alarm's OK action at the same topic or nothing ever closes. Set treat_missing_data to notBreaching on metrics that legitimately go idle; the Probable cause card never calls a change proven good on an alarm that can be OK on silence.

Azure Monitor

POST /webhook/azure-monitor/<workspace-slug>[/<token>] — the common alert schema; the adapter reads data.essentials and a payload without it yields no alert. Dynamic-threshold rules send the same essentials and attribute the same way; alertContext is not read.

Field Canonical field Note
essentials.alertId, else the rule name fingerprint
essentials.alertRule labels.alertname
essentials.severity labels.severity Sev0, Sev1 → critical; Sev2 → warning; Sev3, Sev4 → info; anything else → warning.
alertTargetIDs[0], last path segment labels.resource
subscription and resource group inside alertTargetIDs[0] labels.subscription, labels.resource_group
providers/<ns>/<type> inside alertTargetIDs[0] labels.resource_type For example Microsoft.Compute/virtualMachines.
the target resource name labels.service Only when the target is a real resource. A target that is only the rule's scope — a Log Analytics workspace, an Application Insights component (microsoft.operationalinsights/workspaces, microsoft.insights/components), the bare subscription — gets no service: with the triage queue on (the default since 0.1.133) it opens its own held unmapped=<fingerprint> incident; with it off it groups by alertname.
essentials.monitorCondition status Resolved → resolved; everything else → firing.
essentials.description, else the rule annotations.summary
essentials.firedDateTime starts_at

Enable auto-resolve on the alert rule so Resolved is actually sent. For scope-targeted rules, label_map: {"resource": "service"} attributes at resource granularity.

Grafana

POST /webhook/grafana/<workspace-slug> — Grafana unified alerting's Alertmanager-shaped body. Mapping as Alertmanager, with label values coerced to strings. A multi-dimensional rule emits one alert per series; the series label that identifies the workload must be mapped onto service in the rule or through label_map. Leave Disable resolved message off.

Datadog

POST /webhook/datadog/<workspace-slug> — a monitor webhook with the custom payload template:

{"alert_id": "$ALERT_ID", "id": "$ID", "alert_scope": "$ALERT_SCOPE",
 "title": "$ALERT_TITLE", "body": "$EVENT_MSG", "alert_type": "$ALERT_TYPE",
 "alert_transition": "$ALERT_TRANSITION", "tags": "$TAGS", "date": "$DATE"}
Field Canonical field Note
alert_id, else id, plus alert_scope fingerprint $ALERT_ID is the monitor id; $ALERT_SCOPE is the alert group, so without it a multi-alert monitor's second group is deduped away and one group's recovery closes the incident for all. With no id at all, the labels are hashed. Tags never enter the fingerprint.
title, else event_title labels.alertname, annotations.summary A payload with no title and no id yields no alert.
tags — key:value, comma-separated string or list labels.<key> = <value> Where service, env and team come from.
alert_type labels.severity error → critical; warning, warn → warning; info, success → info; anything else → warning. A severity: tag is overwritten.
alert_transition, else transition status Contains recovered → resolved; else firing.
date, else last_updated starts_at Epoch milliseconds.
body annotations.message

Enable the webhook on both the alert and the recovery transitions.

Dynatrace

POST /webhook/dynatrace/<workspace-slug> — a problem notification from a Custom integration (or a Workflow) with the JSON template whose {ImpactedEntities} and {ProblemDetailsJSONv2} placeholders stay unquoted. A Dynatrace problem is already a correlated incident, so one problem is one alert, never one per entity.

Field Canonical field Note
PID, else ProblemID, else the title fingerprint dynatrace:<id> — the same on OPEN and RESOLVED.
State (else ProblemDetailsJSONv2.status) status RESOLVED, CLOSED → resolved; everything else → firing.
ProblemTitle labels.alertname, annotations.summary No PID, no ProblemID and no title → no alert.
ProblemSeverity (else severityLevel) labels.severity AVAILABILITY, ERROR → critical; PERFORMANCE, RESOURCE_CONTENTION, CUSTOM_ALERT → warning; MONITORING_UNAVAILABLE, INFO → info; missing → warning.
Tags (key:value, comma-separated), else the v2 entityTags labels.<key> = <value> A [context] prefix is dropped; a tag with no value is ignored.
a service: tag, else the first impacted SERVICE, else APPLICATION, else the first impacted entity labels.service
PID, ProblemID, ProblemImpact labels.dynatrace_problem_id, labels.dynatrace_display_id, labels.dynatrace_impact
ImpactedEntities labels.impacted_entities (names, capped at 300 chars), labels.dynatrace_entity_type (the first entity's type)
ProblemDetailsJSONv2.rootCauseEntity.name labels.dynatrace_root_cause
ProblemDetailsJSONv2.startTime starts_at Epoch ms.
ProblemURL, ProblemDetailsText annotations.problem_url, annotations.details (2,000 chars)

Send test notification posts the template with sample values and is answered 200 with "received": 1, "alerts_created": 1 and one incident on service MyService1; resolve that incident by hand unless a RESOLVED for the same test PID follows. A 400 request body is not JSON on this path usually means a string placeholder was left unquoted.

Robusta

POST /webhook/robusta/<workspace-slug> — a webhook_sink finding, including the Kubernetes-native findings (CrashLoopBackOff, OOMKill, ImagePullBackOff) that never reach Alertmanager.

Field Canonical field Note
fingerprint, else a hash of the labels fingerprint
title, else aggregation_key labels.alertname Neither → no alert.
severity labels.severity high → critical; medium, low → warning; info, debug → info; else warning.
subject.labels labels Merged in first, so an explicit service label wins.
subject.name labels.resource, labels.service (fallback) The workload name is the service unless labelled.
subject.namespace, subject.node, subject.container, subject.kind, cluster_name labels.namespace, labels.node, labels.container, labels.kind, labels.cluster
description, else the title; subject.annotations annotations.summary, plus the subject annotations
starts_at starts_at

Always firing: Robusta emits no resolved transition, so closure comes from another source clearing, a remediation or a person.

Security Hub

POST /webhook/securityhub/<workspace-slug>[/<token>] — ASFF findings delivered by EventBridge (detail.findings), by SNS (a JSON string in Message), posted directly as {"findings": [...]}, or as a bare finding with an Id or Title. Findings are normalised into the compliance shape; one finding of a shape the platform does not model is skipped, not the whole delivery.

Canonical field Value
fingerprint the finding Id, else <control>:<resource ARN>
labels.service control:<ControlId> (for example control:S3.9); an AWS Config rule's finding has no control id and keys on its rule name, without a conformance pack's -conformance-pack-<id> suffix (control:iam-user-mfa-enabled — the same rule in two packs is one control); else cloud-posture — correlation is on the control: twelve buckets failing one control is one piece of work
labels.alertname the control id, else the first 80 characters of the title
labels.severity, labels.finding_family, labels.remediation_stance, labels.remediation_tier, labels.rule_kind the classifier's output (remediation_stance is auto, human or ticket; rule_kind defaults to detective)
labels.account, region, resource, resource_type, product, fix_available, remediation_operation when the finding carries them
annotations.summary, description (2,000 chars), recommended_action, finding_id, resource_arn, vendor_remediation (1,000 chars), vendor_remediation_url, cve_ids (first 10), cvss, vulnerable_packages vulnerable_packages names each vulnerable package once with its installed and fixed version, for example stdlib (GOBINARY) 1.26.8 -> 1.27.2 (first 10). A re-sent finding refreshes these annotations on the alert already open, so the next analysis reads what the scanner reports now
status resolved when the scanner reports the finding cleared (RESOLVED, PASSED); else firing

The finding_family label routes a threat-detected family (GuardDuty) to SecOps and every other family to Compliance; an AWS Config rule is never a threat, whatever service its name mentions (guardduty-enabled-centralized checks that GuardDuty is on). Forward PASSED / RESOLVED too or the clear never arrives. The same findings can be pulled on a schedule instead (aiops_scanner_sync_interval_s, Security posture → Posture → Scanner sync); pull and webhook share the deduplication.

Defender for Cloud

POST /webhook/defender/<workspace-slug> — assessments or security alerts pushed by workflow automation (a Logic App HTTP action): a bare object, or a findings, value, assessments or alerts list, optionally under data. Normalised into the same shape as Security Hub, so one compliance path serves both clouds. Keep the Healthy state in the automation's filter so the clear arrives.

ServiceNow

POST /webhook/servicenow/<workspace-slug> — an outbound REST message from a business rule. ServiceNow has no fixed webhook schema; the record is read from record, resource or the flat body, and each field may be a string or a {display_value, value} pair (name-like fields read the display value, priority and state read the code).

Field Canonical field Note
sys_id, else number fingerprint (snow-<id>) Neither → no alert.
number labels.alertname (servicenow-<number>)
cmdb_ci, else business_service labels.service A ticket about a service already alerting joins that incident.
priority labels.severity 1 → critical; 2 → error; 3 → warning; 4, 5 → info; else warning.
state status 6, 7, resolved, closed → resolved; else firing.
short_description, description (2,000 chars) annotations.summary, annotations.message
number/sys_id, url, 32-hex sys_id annotations.ticket_ref, ticket_url, ticket_api_id The incident adopts the originating ticket and writes its analysis back to it.
correlation_id matching aiops:<incident id> annotations.aiops_mark The platform's own ticket coming back: counted as ticket_echo, never a second incident. Include correlation_id, correlation_display and sys_id in the posted record.
severity; priority, else impact and urgency; assigned_to, else assignment_group; watch_list annotations.ticket_fields Which required fields the ticket names (the names only, never the people).
a Field: value line in description (Service:, Severity:, Priority:, Assignee:, Notify:) annotations.ticket_fields; Service: → labels.service when there is no CI, Severity: → labels.severity A value that starts with a placeholder (TBD, n/a, none, -) names nothing.
opened_at starts_at

A record whose sys_class_name starts with change_request, or whose number starts with CHG, is a change and goes to the change ledger instead.

Jira

POST /webhook/jira/<workspace-slug> — jira:issue_created and jira:issue_updated only; comment events (the platform's own RCA write-back included) yield nothing.

Field Canonical field Note
issue.key fingerprint (jira-<key>), labels.alertname (jira-<key>) No key → no alert.
fields.components[0].name labels.service
fields.priority.name labels.severity highest → critical; high → error; medium → warning; lower → info; else warning.
fields.status.statusCategory.key status done → resolved; else firing.
fields.summary, fields.description (plain text only, 2,000 chars) annotations.summary, annotations.message Atlassian Document Format descriptions are not carried.
issue.key, <site>/browse/<key> annotations.ticket_ref, ticket_url, ticket_api_id
a label aiops-<incident id> annotations.aiops_mark The platform's own ticket echo.
fields.priority, fields.assignee; a Field: value line in the description or a field:value label (service:, severity:, notify: …) annotations.ticket_fields Which required fields the issue names (the names only). A Service: line or service: label is labels.service when there is no component; a Severity: one is labels.severity.
fields.created starts_at

An issue whose type name contains change goes to the change ledger instead.

Azure DevOps

POST /webhook/azure-devops/<workspace-slug> — service hooks. workitem.created and workitem.updated are signals; git.push and git.pullrequest.merged are changes; every other event type yields nothing.

workitem.updated is read from the work item as it now is (resource.revision.fields, id resource.workItemId). A state change is a signal. An edit of System.Tags, System.AssignedTo, Severity, Priority, System.Description, Microsoft.VSTS.TCM.ReproSteps or System.AreaPath only moves on a work item held for missing fields (Tickets that come in must name five fields) and otherwise yields nothing; any other update — a comment, the platform's own write-back — yields nothing.

Field Canonical field Note
resource.id (created), resource.workItemId else revision.id (updated) fingerprint (ado-<id>), labels.alertname (ado-<id>)
Microsoft.VSTS.Common.Severity, else Microsoft.VSTS.Common.Priority (first character) labels.severity 1 → critical; 2 → error; 3 → warning; 4 → info; else warning.
System.AreaPath (last segment) labels.area_path Select area_path under Map this source to make it the service.
a service:<name> tag, else a Service: <name> line in System.Description or Repro Steps (a Bug's body) labels.service No service field of its own; without either, map area_path, use label_map or the generic endpoint.
Microsoft.VSTS.Common.Severity, Microsoft.VSTS.Common.Priority, System.AssignedTo; a notify:<team> tag or a Field: value line in the description or Repro Steps annotations.ticket_fields Which required fields the work item names (the names only).
System.Title, System.Description (2,000 chars) annotations.summary, annotations.message
work item id, _links.html.href (updated: the resource's, else the revision's) annotations.ticket_ref, ticket_url, ticket_api_id
tag aiops plus a #<8 hex> title mark annotations.aiops_mark The platform's own ticket echo.
System.CreatedDate starts_at
System.State status Done, Closed, Resolved or Removed → resolved; anything else firing.

Change sources

A change never opens an incident. These sources write rows to the change ledger — the answer to what changed last on this service, ranked on the incident's Probable cause card and flagged when it falls in a freeze window — and answer {"recorded": N, "flagged_unauthorized": M}. Rows dedupe per source and id, so a redelivered hook is harmless.

Source Events read Row
github, gitea push (ref + commits) and a merged pull request (pull_request.merged true) push / pr_merged, repo repository.full_name
gitlab Push Hook and a Merge request Hook with action: merge or state: merged push / pr_merged, repo project.path_with_namespace
azure-devops git.push, git.pullrequest.merged push / pr_merged, repo <project>/<repository>
argocd argocd-notifications body {app, revision, status, url, at} or the Application object sync keyed argocd:<app>@<revision> (:drift suffix when OutOfSync), service = the Application name
kubernetes-audit the kube-apiserver audit EventList; completed write verbs by non-system: users; a 401/403 response is a denied attempt one row per write, service = the cluster named in the URL
servicenow records with sys_class_name change_request… or a CHG… number ticket_change keyed snow:<sys_id>, service = cmdb_ci
jira issues whose type name contains change ticket_change keyed jira:<key>, service = customfield_service

A servicenow or jira change ticket accounts for a captured write (unauthorized-change detection) only when it arrives with the workspace's ITSM webhook token instead of the shared one; see Changes.

kubernetes-audit has its own path form, /webhook/kubernetes-audit/<workspace-slug>/<token>/<cluster-name>, because the API server's webhook client drops the query string; with the token in a header, put - in the token segment. Its credential is that cluster's audit token (POST /api/aiops/{workspace}/k8s-audit-token/<cluster-name>, see the audit webhook), never the webhook token: what it posts is recorded as the platform's own audit trail. The shared form (/webhook/kubernetes-audit/<workspace-slug>, with or without ?cluster=) answers 404.

Service resolution for every change row: the event's own service, else the workspace change_service_map {"<repo glob>": "<service>"}, else unmapped (recorded, listed under ''); change_resource_map {"<resource glob>": "<service>" | ["<service>", …]} only adds linked services.

Your own change feed

POST /webhook/change/<workspace-slug>[/<token>] with {"source": "<name>", "events": [...]}; each event carries kind (push, pr_merged, deploy, sync or config) and any of service, repo, ref, sha, id, author, title, url, at, detail. An empty or missing events list is 400 payload must carry a non-empty 'events' list. Events count against the rate limit like alerts.

kind: "config" is a setting changed inside an application: service names the application, id is the application's own audit-row id (deduped per source, service and id), detail is {key, old?, new?, resource_id?, service?}. Values are protected by the key's name only: under a key that looks secret (password, secret, token, key, pass, pwd, dsn, auth, cert, salt, bearer) only key, resource_id and service are kept and old/new read [not stored]; a URL's scheme://user:password@host password is replaced in every kept value. Never send a credential in a change payload.

Test alerts and receipts

Because the public endpoint answers one generic 401, the way to learn whether a sender reached the platform is a validation session: Alert intake & tokens → Send a test alert mints a correlation id, the sample payload carries it as the aiops_validation label with service aiops-validation, and the ingest stamps a receipt onto the session in the same transaction as the alert. Samples exist for alertmanager, generic, datadog and dynatrace; any other source is 422 no sample for '<source>' yet. A session expires after 24 hours.

receipt_state Meaning
none No receipt observed yet — the request never passed token authentication, or has not arrived.
accepted Received; a new incident opened.
accepted_not_actioned Received and its incident opened, but the alert was held at ingest (no run started). Not reached by the shipped samples, which carry a service and an alert name.
correlated Received; joined an incident already open for the same service.
deduplicated An identical alert was already recorded; the receipt points at its incident.

The page also reports attribution_state — service, namespace, alertname or fingerprint — naming which label grouped the alert; anything but service means real alerts from that sender will not join the right incident.

Errors

Status Body detail Cause and fix
401 invalid webhook credentials Wrong token, wrong slug, disabled workspace, or a workspace that has never minted a token — the message is the same for all four. Compare the slug and token with the Webhook ingest card; a header always wins over a path segment.
404 unknown webhook source '<x>' (supported: argocd, azure-devops, azure-monitor, cloudwatch-sns, datadog, defender, dynatrace, gitea, github, gitlab, grafana, jira, kubernetes-audit, robusta, securityhub, servicenow) A source name the shared route does not know. alertmanager, generic and change are separate routes, so they are missing from the list even though they exist.
400 request body is not JSON / request body must be a JSON object The shared route parses the body itself (SNS delivers JSON as text/plain). A string placeholder left unquoted in a template is the usual cause.
400 payload must carry a non-empty 'events' list The change webhook received no events.
400 SubscribeURL is not an https sns.<region>.amazonaws.com URL An SNS confirmation whose URL is not a genuine SNS endpoint is never fetched.
422 FastAPI validation error alertmanager and generic are typed routes: a body that is not a JSON object fails validation with the standard 422 body instead of the 400 above.
413 request body is larger than the <n>-byte webhook limit The body is over aiops_webhook_max_body_bytes (platform default 1 MiB; Alertmanager, Grafana and the generic intake accept 8 MiB because they send a whole alert group per notification; kubernetes-audit accepts 16 MiB because the API server batches events). The size is checked before the body is parsed, from Content-Length or, for a chunked upload, from the bytes received. Split the payload or send fewer alerts per notification (Alertmanager: max_alerts on the webhook_configs entry, which leaves the rest of the group out). The platform administrator can raise the cap with AIOPS_WEBHOOK_MAX_BODY_BYTES. The sender does not retry a 413.
429 ingest rate limit exceeded — retry after the indicated delay or raise settings.aiops_ingest_rate_per_min for this tenant The workspace bucket is dry. The Retry-After header says when to resend the whole payload.
200 with "received": 0 — The adapter recognised none of its fields: wrong source path for the payload shape, or the payload lacks the one required field (AlarmName, data.essentials, a Datadog title or id, a Dynatrace PID/ProblemID/title, a ticket number).

A 413 can also come from an ingress or proxy in front of the platform before the request reaches it: ingress-nginx refuses bodies over its proxy-body-size (1 MiB by default).

Next steps