All endpoints are served by the Go backend on the configured port (default 8080). CORS is enabled for all origins.
Returns OK with status 200 if the backend is running.
curl http://localhost:8080/health
# OKSubmit a new change event (e.g., from a CI/CD pipeline).
Request Body:
{
"trigger_type": "CI",
"change_type": "build_success",
"affected_services": ["payment-service"],
"summary": "Building payment-service v2.4.0",
"timestamp": "2026-01-31T15:30:00Z",
"end_time": "2026-01-31T15:32:00Z",
"metadata": {
"git_commit_sha": "a1b2c3d4e5f6",
"image_tag": "v2.4.0"
}
}| Field | Type | Required | Description |
|---|---|---|---|
trigger_type |
string | yes | "CI", "GitOps", or "manual" |
change_type |
string | yes | e.g., "build_success", "deployment_rollout", "configmap_update" |
affected_services |
string[] | yes | Services affected by this change |
summary |
string | no | Human-readable description |
timestamp |
string (RFC3339) | yes | When the change started |
end_time |
string (RFC3339) | no | When the change completed |
metadata |
object | no | Arbitrary string key-value pairs. Well-known keys (git_commit_sha, image_tag) drive linking and diffing. All other keys are displayed in the UI — values that are plain https:// URLs render as clickable links; values containing **bold**, *italic*, `code`, or [text](url) markdown syntax are rendered inline. |
Response: 201 Created (empty body)
Errors:
400 Bad Request- Invalid JSON payload, ortimestamp/end_timeare too far in the future.
List change events with optional filtering and pagination.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
int | Max results to return (default 50, max 200) |
offset |
int | Number of results to skip |
service |
string | Filter by service name(s), comma-separated |
namespace |
string | Filter by Kubernetes namespace |
change_type |
string | Filter by change type |
from |
string (RFC3339) | Start of time range |
to |
string (RFC3339) | End of time range |
search |
string | Search in summary and metadata |
Response:
{
"events": [
{
"id": "evt-abc123",
"trigger_type": "GitOps",
"change_type": "deployment_rollout",
"affected_services": ["payment-service"],
"timestamp": "2026-01-31T15:30:00Z",
"end_time": "2026-01-31T15:32:00Z",
"metadata": {
"git_commit_sha": "a1b2c3d",
"repository_url": "https://github.com/my-org/my-repo"
},
"summary": "Rolled out payment-service v2.4.0",
"status": "ready",
"invalid_reason": "",
"skew_seconds": 0,
"contextual_links": [
{
"name": "View Commit on GitHub",
"url": "https://github.com/my-org/my-repo/commit/a1b2c3d"
}
]
}
],
"total": 42,
"limit": 50,
"offset": 0
}The status field indicates the event's validity state and its eligibility for analysis:
"ready"- Event is valid and eligible for analysis (or has already been analyzed)."invalid_time"- Event'stimestamporend_timewas too far in the future at ingestion (more than 2 minutes from the backend's clock), preventing it from being analyzed. This event will be ignored by the analysis worker.
| Field | Type | Description |
|---|---|---|
id |
string | Unique identifier for the event |
trigger_type |
string | "CI", "GitOps", or "manual" |
change_type |
string | e.g., "deployment_rollout", "build_success" |
affected_services |
string[] | Services affected by this change |
summary |
string | Human-readable description |
timestamp |
string (RFC3339) | When the change started |
end_time |
string (RFC3339) | When the change completed (optional) |
metadata |
object | Key-value pairs |
status |
string | Indicates the event's validity state ("ready" or "invalid_time") |
invalid_reason |
string | If status is "invalid_time", provides the reason (e.g., "timestamp_in_future") |
skew_seconds |
int | If status is "invalid_time", indicates how many seconds into the future the timestamp was |
contextual_links |
object[] | Generated deep links to external systems |
Trigger impact analysis for a specific change event.
Request Body:
{
"event_id": "evt-abc123"
}Response (success): 200 OK
{
"change_event": { "..." },
"is_orphaned": false,
"baseline_metrics": {
"error_rate": 0.02,
"latency_p95_ms": 150.5,
"rps": 1200.0,
"cpu": 0.45,
"memory": 512000000,
"additional_metrics": { "orders_per_minute": 85.3 }
},
"impact_metrics": { "..." },
"deltas": { "..." },
"impact_score": 0.65,
"impact_level": "MEDIUM",
"confidence_score": 1.0
}Response (window not closed): 422 Unprocessable Entity
{
"change_event": { "..." },
"impact_level": "PENDING"
}This is returned when analysis is requested before the post-execution impact window has elapsed. Retry after the window closes (default 30 minutes after rollout_end + 5-minute buffer).
Errors:
400 Bad Request- Invalid JSON payload404 Not Found- Event ID does not exist500 Internal Server Error- Analysis failed (Prometheus unreachable, database error, etc.)
List all services that have at least one change event.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
namespace |
string | Filter by Kubernetes namespace. When kubernetes.namespaces is configured, the namespace is validated against the config - unconfigured namespaces return an empty list. |
linked_only |
string | Set to "true" to return only services that have intent-execution links (sha_match, image_tag_match, or image_sha_inferred). |
Response: 200 OK
["payment-service", "order-service", "user-service"]List monitored namespaces. When kubernetes.namespaces is configured in config.yaml, returns exactly those namespaces (sorted). When unconfigured, falls back to discovering namespaces from stored events in the database.
Response: 200 OK
["default", "payment-app"]List all available metrics (core + additional from config).
Response: 200 OK
[
{
"name": "error_rate",
"icon": "AlertCircle",
"weight": 0.4,
"type": "builtin",
"description": "Rate of HTTP errors (status 5xx) for a service.",
"promql": "avg_over_time(sum(rate(http_requests_total{service=~\"{{ .Services }}\",status=~\"5..\"}[1m]))[{{ .Duration }}])"
},
{
"name": "orders_per_minute",
"icon": "ShoppingCart",
"weight": 0.1,
"type": "custom",
"description": "Custom metric: orders_per_minute",
"query": "sum(rate(app_orders_total{service=~\"{{ .Services }}\"}[1m])) * 60"
}
]
| Field | Type | Description |
|:------|:-----|:------------|
| `name` | string | Unique identifier for the metric |
| `icon` | string | Optional. Lucide icon name for UI display |
| `weight` | float | Normalized weight (0.0 - 1.0) contributing to the overall impact score |
| `type` | string | Metric type: `"builtin"` or `"custom"` |
| `description` | string | Human-readable description of the metric |
| `promql` | string | PromQL query template for built-in metrics |
| `query` | string | Actual PromQL query string for custom metrics |Rank concurrent changes for a service by likelihood of causing degradation.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
service |
string | yes | Service name to rank changes for |
from |
string (RFC3339) | yes | Start of time range |
to |
string (RFC3339) | yes | End of time range |
Example:
curl "http://localhost:8080/api/v1/rankings?service=payment-service&from=2026-01-31T14:00:00Z&to=2026-01-31T16:00:00Z"Response: 200 OK
{
"service": "payment-service",
"from": "2026-01-31T14:00:00Z",
"to": "2026-01-31T16:00:00Z",
"ranked": [
{
"analysis": { "..." },
"rank": 1,
"likelihood_score": 0.78,
"temporal_proximity": 0.9,
"change_type_weight": 1.0,
"service_scope": 1.0
}
],
"total": 3
}Errors:
400 Bad Request- Missingservice,from, ortoparameters, or invalid date format
Get the list of metric names enabled for display in the UI for a specific service.
Response: 200 OK
["error_rate", "latency_p95_ms", "orders_per_minute"]Set which metrics to display in the UI for a specific service.
Request Body:
["error_rate", "latency_p95_ms", "rps", "orders_per_minute"]Response: 200 OK (empty body)
Errors:
400 Bad Request- Invalid JSON payload
All error responses return a plain text error message in the response body with an appropriate HTTP status code.
Common status codes:
400- Bad request (invalid input)404- Resource not found405- Method not allowed422- Unprocessable entity (semantic error, e.g., impact window not closed)500- Internal server error