Service topology
Overview
A service topology is a graph of one service node, its environment nodes, and the resource nodes under them. The graph is a DAG of nodes and edges: a resource can hang from an environment or from another resource of the same environment, and it can have several parents (a database used by two applications). Every topology is bound to exactly one service, and a service has at most one topology.
This API creates and edits topologies, maps the service's active alerts onto the nodes (assignments), proposes nodes from alert data (suggest), and manages the weight policies that set an alert weight. The graph editor in the web UI uses the same endpoints.
Saving a topology also writes its resource-to-resource edges into the resource graph as relations with source topology, so policies and cause-and-effect rules can use the hierarchy you draw.
Authentication and permissions
All endpoints take an API key:
Authorization: Key YOUR_API_KEY_HERE
| Action | Scope |
|---|---|
| List or read topologies, assignments, suggestions, weight policies | read:service_topologies |
| Create, update, or delete topologies and weight policies (including bulk) | write:service_topologies |
| Export a topology or a bundle | read:service_topologies |
| Import topologies | write:service_topologies |
See API keys for scopes. Creating a topology also requires the Service Topology feature in your license and a free slot in its topology limit (403 otherwise).
Topology object
{
"id": "3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01",
"name": "Orders",
"description": "Orders service topology",
"service": "orders",
"serviceKpi": null,
"orientation": "TB",
"nodes": [
{
"id": "0d1e2f3a-4b5c-4d6e-8f90-a1b2c3d4e5f6",
"kind": "service",
"label": "orders",
"environment": null,
"resource": null,
"icon": "fa:server",
"params": {},
"hierarchyConfig": {},
"position": { "x": 100, "y": 100 }
},
{
"id": "1e2f3a4b-5c6d-4e7f-9a01-b2c3d4e5f6a7",
"kind": "environment",
"label": "Production",
"environment": "Production",
"resource": null,
"icon": "fa:server",
"params": {},
"hierarchyConfig": {},
"position": { "x": 100, "y": 250 }
},
{
"id": "2f3a4b5c-6d7e-4f80-a112-c3d4e5f6a7b8",
"kind": "resource",
"label": "orders-app",
"environment": "Production",
"resource": "orders-app",
"icon": "fa:server",
"params": {},
"hierarchyConfig": { "disabledHereditary": false },
"position": { "x": 100, "y": 400 }
},
{
"id": "3a4b5c6d-7e8f-4091-b223-d4e5f6a7b8c9",
"kind": "resource",
"label": "orders-db",
"environment": "Production",
"resource": "orders-db",
"icon": "fa:database",
"params": {},
"hierarchyConfig": {},
"position": { "x": 100, "y": 550 }
}
],
"edges": [
{ "id": "e1", "source": "0d1e2f3a-4b5c-4d6e-8f90-a1b2c3d4e5f6", "target": "1e2f3a4b-5c6d-4e7f-9a01-b2c3d4e5f6a7" },
{ "id": "e2", "source": "1e2f3a4b-5c6d-4e7f-9a01-b2c3d4e5f6a7", "target": "2f3a4b5c-6d7e-4f80-a112-c3d4e5f6a7b8" },
{ "id": "e3", "source": "2f3a4b5c-6d7e-4f80-a112-c3d4e5f6a7b8", "target": "3a4b5c6d-7e8f-4091-b223-d4e5f6a7b8c9" }
],
"schemaVersion": 2,
"createTime": "2026-10-09T08:15:00.000Z",
"updateTime": "2026-10-09T09:30:00.000Z"
}
| Field | Meaning |
|---|---|
id | Topology ID |
name / description | Shown in the topology list and the editor header. Both are required when creating |
service | The service this topology describes. Unique across topologies and immutable after creation |
serviceKpi | Optional ID of the Service KPI linked to the topology (service_kpi is also returned for older clients) |
orientation | Layout of the graph: TB (top-bottom), LR (left-right), or CI (circular). Default TB |
nodes / edges | The graph — see below |
schemaVersion | 2 for topologies stored as nodes and edges; 1 for an old tree-based topology that has not been migrated yet and is converted on the fly (see Migration of older topologies) |
createTime / updateTime | When the record was created and last saved |
Node
| Field | Meaning |
|---|---|
id | Stable node ID (any non-empty string; the editor uses UUIDs). Unique within the topology and across topologies |
kind | service, environment, or resource |
label | Node name, at most 255 characters. For an environment node it is the environment name; for a resource node it is the resource name |
environment | Derived by the server on save: the environment the node belongs to (null for the service node) |
resource | Derived by the server on save: the resource name for resource nodes, null otherwise |
icon | Icon identifier (for example fa:database), "" when none |
params | Free object for extra data (the editor stores the node Description here) |
hierarchyConfig | Alert hierarchy settings of the node. Allowed keys: hereditary, disabledHereditary, disabledSLA, disabledPerformance, severityLevelsSLA, severityLevelsPerformance, severityLevelsSLAAvailability, severityLevelsSLAPerformance. Any other key is rejected |
position | { "x": number, "y": number } on the canvas |
Values you send in environment and resource are ignored: the server recomputes them from the graph (the environment is the environment node reachable upwards from the resource).
Edge
| Field | Meaning |
|---|---|
id | Edge ID. Generated when omitted |
source | ID of the parent node |
target | ID of the child node |
Edges point downwards: service → environment, environment → resource, resource → resource. A repeated (source, target) pair is silently ignored.
Validation rules
POST and PUT validate the whole graph before anything is stored. When it is invalid the response is 400 with message: "invalid topology graph" and an errors list, one entry per problem:
{
"status": "error",
"message": "invalid topology graph",
"code": 400,
"errors": [
{ "code": "cycle", "path": "edges", "message": "cycle: a → b → a" },
{ "code": "resourceWithoutEnvironment", "path": "nodes[3a4b5c6d-…]", "message": "resource \"orders-db\" is not reachable from an environment" }
],
"requestId": "…"
}
path points at the offending node or edge (nodes[<index>] or nodes[<index>].label for malformed input, nodes[<id>] for structural rules, edges[<index>] or edges for edge rules).
| Code | Rule |
|---|---|
serviceNodeCount | Exactly one node of kind service is required |
cycle | The edges form a loop. The message lists the node IDs of the cycle |
serviceHasParent | The service node cannot be the target of an edge |
environmentNotUnderService | An environment node must have exactly one parent: the service node |
duplicateEnvironment | Two environment nodes share the same label |
resourceUnderService | A resource node cannot hang directly from the service node |
resourceWithoutEnvironment | A resource node is not reachable from any environment node |
resourceInMultipleEnvironments | Following its parents, a resource node reaches two different environments. A resource that exists in two environments needs one node per environment |
duplicateResource | Two resource nodes have the same label inside the same environment. Give a shared resource one node with several parents instead |
duplicateId | Two nodes share an id |
idCollision | A node id is already used by another topology (PUT only; the message names the ID) |
selfLoop | An edge whose source equals its target |
edgeEndpointMissing | An edge without source / target, or pointing at a node that is not in nodes |
missingId, missingLabel, invalidKind, invalidNode, invalidEdge | A node or edge is not an object or lacks a required field; kind is not service / environment / resource |
valueTooLong | A label longer than 255 characters |
invalidPosition, invalidParams, invalidHierarchyConfig | position.x / position.y are not numbers; params or hierarchyConfig is not an object, or hierarchyConfig has an unknown key |
invalidNodes, invalidEdges, tooLarge | nodes / edges is not a list, or more than 5,000 nodes |
Malformed-input errors are reported first; the structural rules (cycle, environments, resources) only run once every node and edge is well-formed and there is one service node.
List topologies
GET /api/service_topology
Authorization: Key YOUR_API_KEY
Returns a paginated list of topology objects, graph included.
Query parameters
| Parameter | Description | Example |
|---|---|---|
id, name, description, service, service_kpi, orientation, schemaVersion | Filter by column | ?service=orders |
createTime, updateTime | Filter by date | ?updateTime=2026-10-01 |
sort-by | Sort column; prefix with - for descending (name, service, createTime, updateTime, …) | ?sort-by=-updateTime |
page / page-size | Pagination | ?page=2&page-size=50 |
Example
Most recently saved topologies first
- Bash
- Python
curl -X GET "http://your-tobealert.com:9000/api/service_topology?sort-by=-updateTime&page-size=20" \
-H "Content-Type: application/json" \
-H "Authorization: Key YOUR_API_KEY"
import requests
response = requests.get(
"http://your-tobealert.com:9000/api/service_topology",
headers={
"Content-Type": "application/json",
"Authorization": "Key YOUR_API_KEY"
},
params={
"sort-by": "-updateTime",
"page-size": "20"
}
)
if response.status_code in [200, 201]:
print("Request successful")
print(response.json())
else:
print(f"Error: {response.status_code}")
print(response.text)
Response
{
"status": "ok",
"topologies": [ /* array of topology objects */ ],
"total": 3,
"page": 1,
"pageSize": 20,
"pages": 1,
"more": false
}
Get a topology
GET /api/service_topology/<topology_id>
Authorization: Key YOUR_API_KEY
| Parameter | Description |
|---|---|
column | id (default) or service_name to look the topology up by its service instead of its ID |
Read the topology of the service 'orders'
- Bash
- Python
curl -X GET "http://your-tobealert.com:9000/api/service_topology/orders?column=service_name" \
-H "Content-Type: application/json" \
-H "Authorization: Key YOUR_API_KEY"
import requests
response = requests.get(
"http://your-tobealert.com:9000/api/service_topology/orders",
headers={
"Content-Type": "application/json",
"Authorization": "Key YOUR_API_KEY"
},
params={
"column": "service_name"
}
)
if response.status_code in [200, 201]:
print("Request successful")
print(response.json())
else:
print(f"Error: {response.status_code}")
print(response.text)
Response
{
"status": "ok",
"total": 1,
"topology": { /* topology object */ }
}
| Code | Reason |
|---|---|
400 | column other than id / service_name |
404 | No topology with that ID or service |
Create a topology
POST /api/service_topology
Content-Type: application/json
Authorization: Key YOUR_API_KEY
Creates the topology record and its graph. Requires write:service_topologies.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name | String | Yes | Display name |
description | String | Yes | Description |
service | String | Yes | Service ID. Must not already have a topology |
serviceKpi | String | No | Linked Service KPI ID |
orientation | String | No | TB, LR, or CI |
nodes | Array | No | Graph nodes. When omitted, the topology starts with a single service node and no edges |
edges | Array | No | Graph edges ([] when omitted). Validated together with nodes |
Example
Create an empty topology (service node only) for the service 'orders'
- Bash
- Python
curl -X POST "http://your-tobealert.com:9000/api/service_topology" \
-H "Content-Type: application/json" \
-H "Authorization: Key YOUR_API_KEY" \
-d '{
"name": "Orders",
"description": "Orders service topology",
"service": "orders",
"orientation": "TB"
}'
import requests
response = requests.post(
"http://your-tobealert.com:9000/api/service_topology",
headers={
"Content-Type": "application/json",
"Authorization": "Key YOUR_API_KEY"
},
json={
"name": "Orders",
"description": "Orders service topology",
"service": "orders",
"orientation": "TB"
}
)
if response.status_code in [200, 201]:
print("Request successful")
print(response.json())
else:
print(f"Error: {response.status_code}")
print(response.text)
Response
201 Created:
{
"status": "ok",
"id": "3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01",
"topology": { /* topology object */ }
}
Errors
| Code | Reason |
|---|---|
400 | name, description, or service missing; invalid orientation; invalid graph (with the errors list) |
403 | Feature not enabled or topology limit reached for your license |
409 | The service already has a topology |
Update a topology
PUT /api/service_topology/<topology_id>
Content-Type: application/json
Authorization: Key YOUR_API_KEY
Updates the record fields and, when nodes and edges are sent, replaces the whole graph. Requires write:service_topologies. This is what Save changes in the graph editor calls.
Request body
Any subset of name, description, serviceKpi, orientation, nodes, edges. Rules:
nodesandedgesmust be sent together; the graph you send replaces the stored one entirely (nodes you leave out are deleted).servicecannot change (400if a different value is sent).id,schemaVersion,createTime, andupdateTimeare ignored; any other field is rejected (400 unknown fields).- Node IDs must not be in use by another topology (
idCollision). - The graph is validated with the validation rules before it is stored; an invalid graph changes nothing.
Example
Replace the graph: orders-db hangs from two parents in Production
- Bash
- Python
curl -X PUT "http://your-tobealert.com:9000/api/service_topology/3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01" \
-H "Content-Type: application/json" \
-H "Authorization: Key YOUR_API_KEY" \
-d '{
"nodes": [
{
"id": "svc",
"kind": "service",
"label": "orders",
"icon": "fa:server",
"position": {
"x": 100,
"y": 100
}
},
{
"id": "env-prod",
"kind": "environment",
"label": "Production",
"position": {
"x": 100,
"y": 250
}
},
{
"id": "orders-app",
"kind": "resource",
"label": "orders-app",
"position": {
"x": 0,
"y": 400
}
},
{
"id": "billing-app",
"kind": "resource",
"label": "billing-app",
"position": {
"x": 200,
"y": 400
}
},
{
"id": "orders-db",
"kind": "resource",
"label": "orders-db",
"icon": "fa:database",
"position": {
"x": 100,
"y": 550
}
}
],
"edges": [
{
"source": "svc",
"target": "env-prod"
},
{
"source": "env-prod",
"target": "orders-app"
},
{
"source": "env-prod",
"target": "billing-app"
},
{
"source": "orders-app",
"target": "orders-db"
},
{
"source": "billing-app",
"target": "orders-db"
}
]
}'
import requests
response = requests.put(
"http://your-tobealert.com:9000/api/service_topology/3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01",
headers={
"Content-Type": "application/json",
"Authorization": "Key YOUR_API_KEY"
},
json={
"nodes": [
{
"id": "svc",
"kind": "service",
"label": "orders",
"icon": "fa:server",
"position": {
"x": 100,
"y": 100
}
},
{
"id": "env-prod",
"kind": "environment",
"label": "Production",
"position": {
"x": 100,
"y": 250
}
},
{
"id": "orders-app",
"kind": "resource",
"label": "orders-app",
"position": {
"x": 0,
"y": 400
}
},
{
"id": "billing-app",
"kind": "resource",
"label": "billing-app",
"position": {
"x": 200,
"y": 400
}
},
{
"id": "orders-db",
"kind": "resource",
"label": "orders-db",
"icon": "fa:database",
"position": {
"x": 100,
"y": 550
}
}
],
"edges": [
{
"source": "svc",
"target": "env-prod"
},
{
"source": "env-prod",
"target": "orders-app"
},
{
"source": "env-prod",
"target": "billing-app"
},
{
"source": "orders-app",
"target": "orders-db"
},
{
"source": "billing-app",
"target": "orders-db"
}
]
}
)
if response.status_code in [200, 201]:
print("Request successful")
print(response.json())
else:
print(f"Error: {response.status_code}")
print(response.text)
Response
{
"status": "ok",
"topology": { /* topology object with the stored graph */ }
}
Errors
| Code | Reason |
|---|---|
400 | Body not an object, unknown field, service changed, only one of nodes / edges, empty name, invalid orientation, invalid graph (errors list), or idCollision |
404 | Topology not found |
Resource graph synchronization
After every successful save (and on create), each resource → resource edge whose two nodes are in the same environment is stored in the resource graph as a relation {environment, resource: <child>, parent: <parent>, source: "topology", sourceRef: <topology id>}. Relations of this topology that no longer match an edge are removed. Service → environment and environment → resource edges are not written (they are not resource relations).
Relations follow the usual source ranking (manual > event > cmdb > topology > policy): a topology relation never overwrites one declared manually, by an alert, or by a CMDB, but it does take precedence over relations written by policies. You can list what a topology wrote with GET /api/resources/relations?source=topology&sourceRef=<topology id>.
Delete a topology
DELETE /api/service_topology/<topology_id>
Authorization: Key YOUR_API_KEY
Deletes the topology, its graph, and the resource graph relations it wrote (source: topology, sourceRef = this ID). Requires write:service_topologies.
Delete a topology
- Bash
- Python
curl -X DELETE "http://your-tobealert.com:9000/api/service_topology/3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01" \
-H "Content-Type: application/json" \
-H "Authorization: Key YOUR_API_KEY"
import requests
response = requests.delete(
"http://your-tobealert.com:9000/api/service_topology/3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01",
headers={
"Content-Type": "application/json",
"Authorization": "Key YOUR_API_KEY"
}
)
if response.status_code in [200, 201]:
print("Request successful")
print(response.json())
else:
print(f"Error: {response.status_code}")
print(response.text)
Response
{
"status": "ok",
"topology": "3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01",
"message": "topology deleted sucessfully"
}
| Code | Reason |
|---|---|
404 | Topology not found |
Bulk-delete topologies
POST /api/service_topologies/bulk/delete
Content-Type: application/json
Authorization: Key YOUR_API_KEY
Deletes several topologies at once, including their resource graph relations. Requires write:service_topologies. This is what Delete topology on a multi-row selection in the web UI calls.
| Field | Type | Required | Description |
|---|---|---|---|
service_topologies | Array | Yes | Topology IDs, as strings or as { "id": "…" } objects |
Delete two topologies
- Bash
- Python
curl -X POST "http://your-tobealert.com:9000/api/service_topologies/bulk/delete" \
-H "Content-Type: application/json" \
-H "Authorization: Key YOUR_API_KEY" \
-d '{
"service_topologies": [
"3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01",
"7b2d9e4f-1c3a-4f5e-8d60-9a1b2c3d4e5f"
]
}'
import requests
response = requests.post(
"http://your-tobealert.com:9000/api/service_topologies/bulk/delete",
headers={
"Content-Type": "application/json",
"Authorization": "Key YOUR_API_KEY"
},
json={
"service_topologies": [
"3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01",
"7b2d9e4f-1c3a-4f5e-8d60-9a1b2c3d4e5f"
]
}
)
if response.status_code in [200, 201]:
print("Request successful")
print(response.json())
else:
print(f"Error: {response.status_code}")
print(response.text)
Response
{
"status": "ok",
"deleted": 2
}
| Code | Reason |
|---|---|
400 | service_topologies empty or without valid IDs |
Alert assignments
GET /api/service_topology/<topology_id>/assignments
Authorization: Key YOUR_API_KEY
Maps the service's active alerts onto the nodes of the topology. The web UI uses it for Show alerts (node colors, badges, the per-node alerts table). Requires read:service_topologies.
The alerts are the same ones returned for the service (GET /api/alerts/by_service/<service>): alerts whose service contains the topology's service, filtered by the impact rules of the service catalog entry unless includeExcluded is set.
Query parameters
| Parameter | Description |
|---|---|
severity | Only alerts with these severities (repeatable: ?severity=critical&severity=major) |
includeExcluded | true to ignore the service's impact filters |
How an alert is assigned
Environment and resource names are trimmed and compared exactly (an empty environment is a value of its own). For each alert:
- Exact match — a resource node with the alert's
environmentandresourcegets the alert. - Roll-up through the resource graph — when the alert's resource is not a node, the server walks the resource's ancestors in the resource graph (the alert's own
resourceParentcounts as the first ancestor even if the graph has no relation for it yet, up to 10 levels) and assigns the alert to the first ancestor that is a node of the same environment. These alerts are also reported inrollups. - Environment node — otherwise, the node of the alert's environment.
- Unassigned — otherwise, the alert is listed in
unassigned.
Aggregating counts upwards (an environment showing the alerts of its resources) and coloring is done by the client.
Example
Critical alerts of the service, assigned to the topology nodes
- Bash
- Python
curl -X GET "http://your-tobealert.com:9000/api/service_topology/3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01/assignments?severity=critical" \
-H "Content-Type: application/json" \
-H "Authorization: Key YOUR_API_KEY"
import requests
response = requests.get(
"http://your-tobealert.com:9000/api/service_topology/3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01/assignments",
headers={
"Content-Type": "application/json",
"Authorization": "Key YOUR_API_KEY"
},
params={
"severity": "critical"
}
)
if response.status_code in [200, 201]:
print("Request successful")
print(response.json())
else:
print(f"Error: {response.status_code}")
print(response.text)
Response
{
"status": "ok",
"total": 3,
"alerts": [ /* array of alert objects, as in the alert API */ ],
"assignments": {
"orders-db": ["a1f3…", "b7c2…"],
"env-prod": ["c9d4…"]
},
"unassigned": [],
"rollups": {
"b7c2…": { "nodeId": "orders-db", "via": "orders-db" }
}
}
| Field | Meaning |
|---|---|
alerts | The alerts considered, full objects |
assignments | Node ID → list of alert IDs assigned to that node |
unassigned | Alert IDs that matched neither a resource nor an environment node |
rollups | Alert ID → { nodeId, via } for alerts assigned through the resource graph: via is the ancestor resource that is a node |
| Code | Reason |
|---|---|
404 | Topology not found |
Suggest nodes from alerts
POST /api/service_topology/<topology_id>/suggest
Content-Type: application/json
Authorization: Key YOUR_API_KEY
Proposes the environment and resource nodes (and edges) that the service's active alerts mention but the topology does not have yet. Nothing is saved: the client adds the suggestions it accepts and then saves with PUT. This is what Alert-driven service topology in the graph editor calls. Requires read:service_topologies.
Request body
| Field | Type | Description |
|---|---|---|
severity | Array | Only alerts with these severities (same as the severity query parameter) |
includeExcluded | Boolean | true to ignore the service's impact filters |
Both can also be passed as query parameters.
How suggestions are built
For every alert whose (environment, resource) is not already a node:
- An environment node is proposed when the alert's environment is not in the topology, hanging from the service node.
- The parent chain of the resource is resolved, from the alert's own
resourceParentfirst and, when the alert has none, from the resource's main parent in the resource graph. The walk continues parent by parent (at most 10 levels, no loops) and stops at the first resource that is already a node. - One resource node is proposed per resource of the chain that is not a node yet, chained with edges from the first existing node (or the environment node) down to the alert's resource.
Alerts without an environment are skipped. Proposed nodes get new IDs, suggested: true, and positions computed with the topology's orientation around the existing nodes.
Example
Propose nodes for the resources that raise alerts on this service
- Bash
- Python
curl -X POST "http://your-tobealert.com:9000/api/service_topology/3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01/suggest" \
-H "Content-Type: application/json" \
-H "Authorization: Key YOUR_API_KEY" \
-d '{
"severity": [
"critical",
"major",
"minor",
"warning"
]
}'
import requests
response = requests.post(
"http://your-tobealert.com:9000/api/service_topology/3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01/suggest",
headers={
"Content-Type": "application/json",
"Authorization": "Key YOUR_API_KEY"
},
json={
"severity": [
"critical",
"major",
"minor",
"warning"
]
}
)
if response.status_code in [200, 201]:
print("Request successful")
print(response.json())
else:
print(f"Error: {response.status_code}")
print(response.text)
Response
{
"status": "ok",
"nodes": [
{
"id": "9c8b7a6f-5e4d-4c3b-a291-0f1e2d3c4b5a",
"kind": "resource",
"label": "vm-7",
"environment": "Production",
"resource": "vm-7",
"icon": "fa:server",
"params": {},
"hierarchyConfig": {},
"position": { "x": 300, "y": 550 },
"suggested": true
}
],
"edges": [
{ "id": "…", "source": "orders-app", "target": "9c8b7a6f-5e4d-4c3b-a291-0f1e2d3c4b5a", "suggested": true }
],
"summary": {
"newEnvironments": [],
"newResources": [{ "environment": "production", "resource": "vm-7" }],
"skippedEnvironments": ["Production"],
"skippedResources": [{ "environment": "production", "resource": "orders-db" }]
}
}
| Field | Meaning |
|---|---|
nodes / edges | Proposed nodes and edges, ready to be added to the graph and saved with PUT |
summary.newEnvironments / newResources | Proposed environment names, and proposed resources as {environment, resource} (the same resource name can exist in two environments) |
summary.skippedEnvironments / skippedResources | Environments and {environment, resource} pairs mentioned by alerts that are already nodes |
| Code | Reason |
|---|---|
404 | Topology not found |
Export a topology
GET /api/service_topology/<topology_id>/export
Authorization: Key YOUR_API_KEY
Returns a self-contained JSON document with everything needed to recreate the service elsewhere: the topology record, the complete graph (IDs and positions included), the weight policies that apply to the service, and the service's catalog entry. The response is sent as an attachment (Content-Disposition: attachment; filename="topology-<service>-<YYYYMMDD-HHmm>.json"). Requires read:service_topologies.
GET /api/service_topology/export?ids=<id1>,<id2>
Returns a bundle with several topologies (topologies-<YYYYMMDD-HHmm>.json). Without ids, the bundle holds every topology. 404 if one of the IDs does not exist.
Export document
{
"kind": "tobealert-service-topology",
"schemaVersion": 2,
"exportedAt": "2026-10-09T10:42:00Z",
"appVersion": "1.12.0",
"topology": {
"id": "3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01",
"name": "Orders",
"description": "Orders service topology",
"service": "orders",
"serviceKpi": null,
"orientation": "TB"
},
"nodes": [ /* node objects, as in GET /api/service_topology/<id> */ ],
"edges": [ /* edge objects */ ],
"weightPolicies": [
{
"id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
"name": "orders-critical-weight",
"description": "Critical alerts of orders weigh 100",
"filters": { "operator": "AND", "clauses": [ { "key": "service", "op": "=", "value": "orders" } ] },
"weight": 100,
"status": "active"
}
],
"catalogService": {
"serviceId": "orders",
"name": "Orders",
"description": "Order intake and fulfilment",
"icon": "fa:cart-shopping",
"alertImpact": { "severities": [], "statuses": [], "whitelist": { "operator": "AND", "clauses": [] }, "blacklist": { "operator": "AND", "clauses": [] } }
}
}
| Key | Content |
|---|---|
kind | tobealert-service-topology; a bundle is tobealert-service-topology-bundle with the documents under topologies |
schemaVersion | 2 — the only version the importer accepts |
exportedAt / appVersion | Informational |
topology | id, name, description, service, serviceKpi, orientation |
nodes / edges | The graph, in the node and edge format of this API |
weightPolicies | Policies tied to the topology (topologyId) plus global policies whose filters contain a clause on the service key with this service as value |
catalogService | The Services catalog entry (serviceId, name, description, icon, alertImpact), or null when the service is not in the catalog |
Export one topology
- Bash
- Python
curl -X GET "http://your-tobealert.com:9000/api/service_topology/3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01/export" \
-H "Content-Type: application/json" \
-H "Authorization: Key YOUR_API_KEY"
import requests
response = requests.get(
"http://your-tobealert.com:9000/api/service_topology/3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01/export",
headers={
"Content-Type": "application/json",
"Authorization": "Key YOUR_API_KEY"
}
)
if response.status_code in [200, 201]:
print("Request successful")
print(response.json())
else:
print(f"Error: {response.status_code}")
print(response.text)
Export every topology as a bundle
- Bash
- Python
curl -X GET "http://your-tobealert.com:9000/api/service_topology/export" \
-H "Content-Type: application/json" \
-H "Authorization: Key YOUR_API_KEY"
import requests
response = requests.get(
"http://your-tobealert.com:9000/api/service_topology/export",
headers={
"Content-Type": "application/json",
"Authorization": "Key YOUR_API_KEY"
}
)
if response.status_code in [200, 201]:
print("Request successful")
print(response.json())
else:
print(f"Error: {response.status_code}")
print(response.text)
Each exported topology writes a service-topology-exported audit entry. The web UI exposes the same files as Export / Export all on the Services tab — see Import and export.
Import topologies
POST /api/service_topology/import?mode=replace|merge
Content-Type: application/json
Authorization: Key YOUR_API_KEY
Creates or updates topologies from an export document, a bundle, or a file from an earlier release. Requires write:service_topologies. Every topology in the request is planned first; if any of them is invalid, nothing is written (400). Use dryRun=1 to get the plan without applying it.
Query parameters
| Parameter | Required | Default | Description |
|---|---|---|---|
mode | Yes | — | replace: the topology becomes exactly the file (nodes, edges and topology-tied weight policies that are not in the file are removed; catalog fields are overwritten). merge: nodes, edges and policies of the file are added or updated, nothing is removed, and only empty catalog fields are filled |
dryRun | No | 0 | 1 / true: validate and return the previews, write nothing |
positions | No | keep | Merge only. keep: existing nodes keep their canvas position; file: positions come from the file |
service | No | — | Service name for a file that does not carry one (a bare tree from an earlier release, or an editor export without topology.service) |
Request body
| Body | Recognised by |
|---|---|
| An export document | kind: tobealert-service-topology |
| A bundle | kind: tobealert-service-topology-bundle — every topology is imported |
An editor export { "topology", "nodes", "edges" } | nodes without kind. weightPolicies and catalogService are optional |
| A tree from an earlier release | { "service": "...", "tree": {...} }, or the tree itself (nodes with type and children); converted on the fly, warning legacyConverted |
multipart/form-data with a file part | Any of the above as a file upload |
The body is limited to 2 MB (tooLarge) and each topology to 5,000 nodes.
How a document is applied
- The topology is identified by
topology.service: it is updated when a topology for that service exists, created otherwise (the license limit applies to creation:403when it is reached).topology.idin the file is ignored. - Node IDs are kept. IDs that belong to another topology are regenerated and the edges remapped (
idsRegeneratedin the preview). - Nodes are matched by identity, not by ID: the service node, environments by name, resources by environment + name. Matched nodes take
label,icon,paramsandhierarchyConfigfrom the file. - After merging, the whole graph is validated again with the validation rules; failures are reported with the
after merge:prefix. - Weight policies are matched by name (across the instance), created or updated with the file's
description,filters,weightandstatus, and tied to the topology. Inreplacemode, policies tied to the topology that are not in the file are deleted. - The catalog service is created if missing. If it exists:
replaceoverwritesdescription,iconandalertImpactwith non-empty values from the file;mergeonly fills fields that are empty (descriptionempty, iconserver,alertImpactwithout rules). - The topology's resource-to-resource edges are synchronized to the resource graph (source
topology), as onPUT.
The full comparison of both modes is in Import and export — Replace or merge.
Example
Dry run of a merge into the orders topology
- Bash
- Python
curl -X POST "http://your-tobealert.com:9000/api/service_topology/import?mode=merge&dryRun=1" \
-H "Content-Type: application/json" \
-H "Authorization: Key YOUR_API_KEY" \
-d '{
"kind": "tobealert-service-topology",
"schemaVersion": 2,
"topology": {
"service": "orders",
"name": "Orders",
"description": "Orders service topology",
"orientation": "TB"
},
"nodes": [
{
"id": "svc-orders",
"kind": "service",
"label": "orders",
"position": {
"x": 100,
"y": 100
}
},
{
"id": "env-prod",
"kind": "environment",
"label": "Production",
"position": {
"x": 100,
"y": 250
}
},
{
"id": "res-orders-app",
"kind": "resource",
"label": "orders-app",
"position": {
"x": 100,
"y": 400
}
},
{
"id": "res-orders-db",
"kind": "resource",
"label": "orders-db",
"icon": "fa:database",
"position": {
"x": 100,
"y": 550
}
}
],
"edges": [
{
"id": "e1",
"source": "svc-orders",
"target": "env-prod"
},
{
"id": "e2",
"source": "env-prod",
"target": "res-orders-app"
},
{
"id": "e3",
"source": "res-orders-app",
"target": "res-orders-db"
}
],
"weightPolicies": [],
"catalogService": null
}'
import requests
response = requests.post(
"http://your-tobealert.com:9000/api/service_topology/import",
headers={
"Content-Type": "application/json",
"Authorization": "Key YOUR_API_KEY"
},
params={
"mode": "merge",
"dryRun": "1"
},
json={
"kind": "tobealert-service-topology",
"schemaVersion": 2,
"topology": {
"service": "orders",
"name": "Orders",
"description": "Orders service topology",
"orientation": "TB"
},
"nodes": [
{
"id": "svc-orders",
"kind": "service",
"label": "orders",
"position": {
"x": 100,
"y": 100
}
},
{
"id": "env-prod",
"kind": "environment",
"label": "Production",
"position": {
"x": 100,
"y": 250
}
},
{
"id": "res-orders-app",
"kind": "resource",
"label": "orders-app",
"position": {
"x": 100,
"y": 400
}
},
{
"id": "res-orders-db",
"kind": "resource",
"label": "orders-db",
"icon": "fa:database",
"position": {
"x": 100,
"y": 550
}
}
],
"edges": [
{
"id": "e1",
"source": "svc-orders",
"target": "env-prod"
},
{
"id": "e2",
"source": "env-prod",
"target": "res-orders-app"
},
{
"id": "e3",
"source": "res-orders-app",
"target": "res-orders-db"
}
],
"weightPolicies": [],
"catalogService": null
}
)
if response.status_code in [200, 201]:
print("Request successful")
print(response.json())
else:
print(f"Error: {response.status_code}")
print(response.text)
Uploading a file instead of a JSON body:
curl -X POST "http://your-tobealert.com:9000/api/service_topology/import?mode=replace" \
-H "Authorization: Key YOUR_API_KEY" \
-F "file=@topology-orders-20261009-1042.json"
Response
A dry run returns one preview per topology:
{
"status": "ok",
"dryRun": true,
"previews": [
{
"topology": { "service": "orders", "action": "update", "id": "3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01" },
"nodes": { "create": 1, "update": 0, "remove": 0, "unchanged": 3 },
"edges": { "create": 1, "update": 0, "remove": 0, "unchanged": 2 },
"weightPolicies": { "create": 0, "update": 0, "remove": 0, "unchanged": 0 },
"catalogService": { "action": "skip", "fields": [] },
"idsRegenerated": 0,
"warnings": [
{ "code": "positionsKept", "message": "positions of existing nodes were kept" }
],
"errors": []
}
]
}
| Field | Meaning |
|---|---|
topology.action | create or update; id is the existing topology ID (null on create) |
nodes / edges / weightPolicies | Counters create, update, remove, unchanged. remove is only non-zero in replace mode |
catalogService.action | create, update (with the fields that change: description, icon, alertImpact), unchanged, or skip when the document has no catalogService |
idsRegenerated | Node IDs that belonged to another topology and were replaced |
warnings | legacyConverted (with a report of remapped IDs, merged duplicates and dropped orphans), idsRegenerated, positionsKept |
errors | Empty on 200; on 400 the same codes are returned at the top level |
A real import returns the previews plus one result per topology:
{
"status": "ok",
"dryRun": false,
"results": [
{
"service": "orders",
"topologyId": "3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01",
"action": "updated",
"counts": {
"nodes": { "create": 1, "update": 0, "remove": 0, "unchanged": 3 },
"edges": { "create": 1, "update": 0, "remove": 0, "unchanged": 2 },
"weightPolicies": { "create": 0, "update": 0, "remove": 0, "unchanged": 0 }
},
"catalogService": { "action": "skip", "fields": [] }
}
],
"previews": [ /* as in the dry run */ ]
}
action is created or updated. Each topology writes a service-topology-imported audit entry.
Errors
400 with message: "invalid import" and an errors list in the same shape as the validation errors. In a bundle, path is prefixed with topologies[<index>]..
| Code | Reason |
|---|---|
invalidJson | The body is not valid JSON |
invalidFormat | The JSON matches none of the accepted shapes |
unsupportedSchemaVersion | schemaVersion other than 2 |
emptyBundle | A bundle without topologies |
serviceMissing | No service name in the document and no ?service= |
invalidOrientation | topology.orientation is not TB, LR or CI |
tooLarge | Body over 2 MB, or more than 5,000 nodes |
| Any validation code | The document's graph, or the merged graph (after merge: prefix), breaks a rule |
Other responses: 400 with a plain message when mode is missing or positions is invalid; 403 when the import would create a topology beyond the license limit.
Weight policies
A weight policy sets the weight attribute of alerts that match its filters. Policies are evaluated when an alert is received:
- Only active policies are considered, in creation order; the first one whose filters match sets
attributes.weightand the evaluation stops. - An alert that already carries
attributes.weightin the payload is left as it is: policies never overwrite a weight sent by the monitoring system. - The alert history records the policy execution with the policy as actor.
Policies can optionally be tied to a topology with topologyId.
Policy object
{
"id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
"name": "production-critical-weight",
"description": "Critical alerts in Production weigh 100",
"filters": { "clauses": [ /* query builder clauses */ ] },
"weight": 100,
"topologyId": "3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01",
"status": "active",
"createTime": "2026-10-09T08:15:00.000Z"
}
| Field | Meaning |
|---|---|
name / description | Required |
filters | Alert conditions: { "operator": "AND" | "OR", "clauses": [ { "key": "<alert field>", "op": "=", "value": … }, … ] }, the same format the web UI query builder produces (a clause may itself hold clauses for a nested group; operators include =, !=, <, >, LIKE, ~, IN, CONTAINS and their negations). Empty filters match every alert |
weight | Numeric weight written to attributes.weight |
topologyId | Optional ID of the topology the policy belongs to. Must match an existing topology (400 otherwise); null for a global policy |
status | active or inactive. Only active policies are evaluated |
Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
GET | /api/service_topology_policy | List policies. Filters: id, topologyId, name, description, status, weight, createTime; sort-by, page, page-size |
POST | /api/service_topology_policy | Create a policy (201) |
GET | /api/service_topology_policy/<policy_id> | Read one policy |
PUT | /api/service_topology_policy/<policy_id> | Update fields (name, description, filters, weight, topologyId, status) |
DELETE | /api/service_topology_policy/<policy_id> | Delete a policy |
POST | /api/service_topology_policies/bulk/delete | Body { "policies": [ids] } — delete several policies |
POST | /api/service_topology_policies/bulk/update | Body { "policies": [ids], "status": "active" | "inactive" } — enable or disable several policies |
Reading requires read:service_topologies; everything else write:service_topologies. Responses wrap the policy under the key topology ({ "status": "ok", "topology": { /* policy */ } }); the list returns topologies.
Examples
Create a weight policy tied to the orders topology
- Bash
- Python
curl -X POST "http://your-tobealert.com:9000/api/service_topology_policy" \
-H "Content-Type: application/json" \
-H "Authorization: Key YOUR_API_KEY" \
-d '{
"name": "production-critical-weight",
"description": "Critical alerts in Production weigh 100",
"weight": 100,
"topologyId": "3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01",
"filters": {
"operator": "AND",
"clauses": [
{
"key": "environment",
"op": "=",
"value": "Production"
},
{
"key": "severity",
"op": "=",
"value": "critical"
}
]
}
}'
import requests
response = requests.post(
"http://your-tobealert.com:9000/api/service_topology_policy",
headers={
"Content-Type": "application/json",
"Authorization": "Key YOUR_API_KEY"
},
json={
"name": "production-critical-weight",
"description": "Critical alerts in Production weigh 100",
"weight": 100,
"topologyId": "3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01",
"filters": {
"operator": "AND",
"clauses": [
{
"key": "environment",
"op": "=",
"value": "Production"
},
{
"key": "severity",
"op": "=",
"value": "critical"
}
]
}
}
)
if response.status_code in [200, 201]:
print("Request successful")
print(response.json())
else:
print(f"Error: {response.status_code}")
print(response.text)
Active policies of one topology
- Bash
- Python
curl -X GET "http://your-tobealert.com:9000/api/service_topology_policy?topologyId=3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01&status=active" \
-H "Content-Type: application/json" \
-H "Authorization: Key YOUR_API_KEY"
import requests
response = requests.get(
"http://your-tobealert.com:9000/api/service_topology_policy",
headers={
"Content-Type": "application/json",
"Authorization": "Key YOUR_API_KEY"
},
params={
"topologyId": "3f6a1c2e-8b4d-4e0f-9a71-5c2d7e8f9a01",
"status": "active"
}
)
if response.status_code in [200, 201]:
print("Request successful")
print(response.json())
else:
print(f"Error: {response.status_code}")
print(response.text)
Disable a policy
- Bash
- Python
curl -X PUT "http://your-tobealert.com:9000/api/service_topology_policy/5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a" \
-H "Content-Type: application/json" \
-H "Authorization: Key YOUR_API_KEY" \
-d '{
"status": "inactive"
}'
import requests
response = requests.put(
"http://your-tobealert.com:9000/api/service_topology_policy/5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
headers={
"Content-Type": "application/json",
"Authorization": "Key YOUR_API_KEY"
},
json={
"status": "inactive"
}
)
if response.status_code in [200, 201]:
print("Request successful")
print(response.json())
else:
print(f"Error: {response.status_code}")
print(response.text)
See Weight policies for the web UI.
Migration of older topologies
Topologies created before this release were stored as a single JSON tree (a service root with nested environment and resource children). When the backend starts on this release, a migration converts every tree-based topology to nodes and edges:
- Node IDs are regenerated (UUIDs); the old
default/e-<env>/r-<resource>IDs are not kept. - A resource that appeared under two parents in the tree becomes one node with two parents.
- The old tree is kept internally as a backup for one release; the API only exposes the graph.
- Each resource → resource edge is written to the resource graph with source
topology.
Two cases need your attention and are reported in the backend log:
| Situation | Effect |
|---|---|
| Several topologies for the same service | They are reported; keep one and delete the others |
| A tree that breaks a validation rule — typically the same resource under two environments | The topology is not migrated; it is still served, converted on the fly (schemaVersion: 1), until you fix it in the editor and save, which stores it as version 2 |
Audit trail
| Event | When |
|---|---|
service-topology-created | POST /api/service_topology |
service-topology-updated | PUT /api/service_topology/<id> |
service-topology-deleted | DELETE /api/service_topology/<id> |
service-topology-policy-created | POST /api/service_topology_policy |
service-topology-exported | GET /api/service_topology/<id>/export, and once per topology of a bundle export |
service-topology-imported | POST /api/service_topology/import — once per topology created or updated (the message carries the mode and the action) |
Assignments, suggestions, and bulk deletes do not write audit entries.
Best practices
- Send the whole graph on
PUT— a partialnodeslist deletes what it does not contain. Read the topology first, modify, and send it back. - Model shared resources as one node with several parents rather than duplicating the node; duplicates are rejected (
duplicateResource). - Keep one node per environment for resources that exist in several environments — the same
labelunderProductionandStagingis two different nodes. - Use
suggestbefore building by hand — it already knows the parents fromresourceParentand the resource graph. - Tie weight policies to a topology with
topologyIdwhen they only make sense for that service, and remember that the first matching active policy wins: order of creation matters when filters overlap.