Process Radar API Documentation
API Version: 1.0.0
Base URL: https://api.dkyra.com/api/v1Authentication: API Key or JWT Token
Last Updated: 2026-07-03
Overview
Process Radar analyzes ITSM ticket workflows to automatically identify anomalies, bottlenecks, and optimization opportunities across teams. The API provides programmatic access to analysis results, prioritized situations, health metrics, and delegation workflows.
Key Concepts
| Concept | Description |
|---|---|
| Finding | An automatically detected anomaly in your ITSM workflows, for example a team with unusually long processing times, a routing loop between departments, or a seasonal capacity problem |
| Affected Dimensions | The business context of a finding: which team, category, location, or combination is affected |
| Tier | Severity classification: critical, notable, observed |
| Trend | Development over time: improving, worsening, stable, new |
| Confidence | Robustness assessment (low, medium, high) based on data volume and statistical significance |
| Situation | A group of related findings that together describe a business-relevant problem requiring action, such as "Level-1 Support is overwhelmed" instead of 12 separate symptoms |
| Health Score | An A-through-F grade summarizing overall process health, combining throughput, bottleneck risk, and trend indicators |
| Delegation | Assignment of responsibility with feedback loop via magic-link email. Recipients need no login |
| Narrative | AI-generated explanation of a situation in plain language with recommended actions |
| Statement | Human-readable description of the finding, available in German and English |
Authentication
All API requests require authentication via one of:
API Key (Header)
X-API-Key: your-api-keyJWT Bearer Token
Authorization: Bearer your-jwt-tokenObtaining Credentials
API Key: Create via Settings → API Keys in the Process Radar UI.
JWT Token: Use the login endpoint:
POST /api/v1/auth/login
Content-Type: application/json
{
"email": "[email protected]",
"password": "your-password"
}Response:
{
"access_token": "eyJhbGci...",
"token_type": "bearer",
"expires_in": 300
}Access tokens are short-lived (expires_in is in seconds; 5 minutes). Use POST /api/v1/auth/refresh with the refresh token to obtain a new one before expiry. For long-running integrations, prefer API-Key authentication.
Quick Start
Step 1: Upload data
curl -X POST https://api.dkyra.com/api/v1/datasets/upload \
-H "X-API-Key: YOUR_KEY" \
-F "[email protected]" \
-F "[email protected]" \
-F "name=Q1 Analysis"Step 2: Start analysis
curl -X POST https://api.dkyra.com/api/v1/datasets/{dataset_id}/runs \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Q1 Deep Dive"}'Step 3: Get results
curl https://api.dkyra.com/api/v1/executive-summary \
-H "X-API-Key: YOUR_KEY"Core Endpoints
Health Check
GET /healthReturns API status. No authentication required.
Response:
{
"status": "ok"
}Datasets & Upload
#### List Datasets
GET /api/v1/datasetsReturns all datasets for the current tenant, including run counts.
Response:
[
{
"dataset_id": "uuid",
"name": "Q1 2026 Analysis",
"status": "uploaded",
"created_at": "2026-02-09T10:00:00Z",
"run_count": 3,
"ticket_count": 1500,
"event_count": 45000
}
]#### Upload CSV Files
POST /api/v1/datasets/uploadCreates a dataset and uploads CSV files in one step. Uses multipart/form-data.
Form Fields:
| Name | Type | Description |
|---|---|---|
tickets_file | file | Tickets.csv (semicolon-separated) |
history_file | file | TicketHistory.csv (semicolon-separated) |
name | string | Dataset name (default: "Neue Analyse") |
Response (200):
{
"dataset_id": "uuid",
"status": "uploaded",
"validation": {
"tickets_count": 1500,
"events_count": 45000,
"warnings": []
}
}#### Delete Dataset
DELETE /api/v1/datasets/{dataset_id}?confirm=trueDeletes a dataset and ALL associated data. Requires confirm=true as safety guard.
| Status | Meaning |
|---|---|
| 204 | Successfully deleted |
| 400 | Missing confirm=true parameter |
| 404 | Dataset not found |
| 409 | Cannot delete: active run in progress |
Analysis Runs
#### Start Analysis Run
POST /api/v1/datasets/{dataset_id}/runsStarts an analysis run asynchronously. Returns 202 Accepted immediately.
Request Body:
{
"name": "Q1 Deep Dive"
}Response (202):
{
"run_id": "uuid",
"status": "running",
"name": "Q1 Deep Dive",
"message": "Analysis started"
}#### List Runs
GET /api/v1/runsReturns all runs for the current tenant.
Parameters:
| Name | Type | Description |
|---|---|---|
dataset_id | query | Filter by dataset |
status | query | Filter: QUEUED, RUNNING, SUCCEEDED, FAILED |
limit | query | Max results (default: 50) |
offset | query | Pagination offset |
#### Get Run Status
GET /api/v1/runs/{run_id}Response:
{
"run_id": "uuid",
"dataset_id": "uuid",
"status": "SUCCEEDED",
"name": "Q1 Deep Dive",
"findings_count": 181,
"progress_phase": "complete",
"progress_percent": 100,
"message": null,
"error": null,
"created_at": "2026-02-09T10:00:00Z",
"started_at": "2026-02-09T10:00:00Z",
"finished_at": "2026-02-09T10:02:30Z"
}Executive Summary Endpoints
These aggregated endpoints are designed for AI agents, dashboards, and third-party integrations. They return high-level KPIs without requiring knowledge of internal data structures.
#### Executive Summary
GET /api/v1/executive-summaryHigh-level briefing with system health, prioritized situations, and trend information.
Parameters:
| Name | Type | Description |
|---|---|---|
dataset_id | query | Dataset ID (uses first dataset if omitted) |
limit | query | Max priority situations, 1-10 (default: 5) |
Response:
{
"health_score": {
"score": 62,
"grade": "C"
},
"priority_situations": [
{
"situation_id": "sit-abc-123",
"headline": "Level-1-Support: Kapazitätsengpass mit steigender Tendenz",
"situation_type": "capacity_bottleneck",
"impact_hours": 342.5,
"trend": "worsening",
"recommendation": "delegate",
"finding_count": 4,
"tier": "critical"
}
],
"open_delegations": 2,
"trend_summary": {
"direction": "worsening",
"new_situations": 1,
"resolved_situations": 0
},
"period": {
"start": "2024-01-01T00:00:00",
"end": "2024-12-31T00:00:00",
"run_id": "run-xyz-456"
}
}#### Analysis Summary
GET /api/v1/analysis-summaryAggregated analysis KPIs: anomaly distribution, trends, and most affected roles.
Response:
{
"total_findings": 23,
"tier_distribution": {
"critical": 3,
"notable": 8,
"observed": 12
},
"new_findings": 2,
"trend_distribution": {
"worsening": 5,
"stable": 12,
"improving": 4,
"new": 2
},
"top_affected_roles": [
{
"role": "Level-1-Support",
"finding_count": 6,
"dominant_tier": "critical"
}
],
"last_analysis_at": "2024-12-15T14:30:00"
}#### Flow Health
GET /api/v1/flow-healthSystem-wide flow health metrics: bottleneck roles and capacity status.
Response:
{
"roles_in_bottleneck": 2,
"total_active_roles": 8,
"bottleneck_roles": [
{
"role": "Level-1-Support",
"severity": "critical",
"state": "persistent overload"
},
{
"role": "Incident-Management",
"severity": "notable",
"state": "growing overload"
}
],
"health_grade": "C"
}Findings
#### List Findings
GET /api/v1/runs/{run_id}/findingsReturns all detected anomalies for an analysis run with filtering and sorting.
Parameters:
| Name | Type | Description |
|---|---|---|
run_id | path | Analysis run identifier |
tier | query | Filter: critical, notable, observed |
finding_type | query | Filter by finding type identifier |
role | query | Filter by affected role |
category | query | Filter by affected category |
location | query | Filter by affected location |
min_impact | query | Minimum impact in hours |
ticket_status | query | Filter: closed (default), open, all |
workflow_status | query | Filter: active (default), new, delegate, watching, ignore, escalated, all |
sort_by | query | Sort: relevance_score (default), impact_hours, tier |
sort_order | query | Direction: asc or desc |
limit | query | Max results (default: 50, max: 500) |
offset | query | Pagination offset |
Response:
{
"findings": [
{
"finding_id": "cbf7fec4-f9a2-...",
"finding_type": "F1_ROLE_HIGH_DWELL",
"tier": "critical",
"dimensions": ["role"],
"constellation": {
"role": "2nd Level Support"
},
"statements": {
"de": "Die Rolle '2nd Level Support' zeigt eine deutlich höhere Verweildauer als der globale Median (89.4h vs. 12.0h, +645%). Robustheit: Hoch (234 Tickets).",
"en": "Role '2nd Level Support' shows significantly higher dwell time than the global median (89.4h vs. 12.0h, +645%). Robustness: High (234 tickets)."
},
"confidence": {
"level": "high",
"description_de": "Hoch",
"description_en": "High"
},
"volume": {
"ticket_count": 234,
"interval_count": 456
},
"impact_hours": 1250.5,
"relevance_score": 0.82,
"trend": "stable",
"temporal": {
"definition_id": "uuid",
"first_seen": "2024-01-15",
"last_seen": "2024-06-30",
"trend": "stable",
"status": "new"
}
}
],
"total": 181,
"returned": 50,
"filters_applied": {}
}Response fields likefinding_typecontain machine-readable identifiers. Usestatements.de/statements.enfor human-readable display.constellationnames the affected dimension values;dimensionslists which dimensions define the finding.
#### Get Finding Detail
GET /api/v1/runs/{run_id}/findings/{finding_id}Returns full details for a single finding including metrics, affected tickets, and trend data.
The response also carries type_explanation, a static, consumer-facing explanation of the finding type with four fields: what_de / what_en (what this finding type measures) and how_to_read_de / how_to_read_en (lead metric, reference baseline, unit). It never varies per run; the run-specific statement is the finding's kernaussage. null for types without an explanation. Hide the block in that case.
#### Get Finding Time Series
GET /api/v1/findings/{finding_id}/timeseriesReturns weekly time series data for a finding (affected tickets per ISO week), useful for sparklines and trend visualization.
Parameters:
| Name | Type | Description |
|---|---|---|
finding_id | path | Finding identifier |
run_id | query | Required. Analysis run identifier |
view | query | last_52_weeks (default) or full_history |
as_of | query | Time anchor (ISO-8601 UTC; default: latest event in the run) |
Response:
{
"finding_id": "uuid",
"run_id": "uuid",
"as_of": "2026-01-03T15:05:42Z",
"view": "last_52_weeks",
"bucket": "week",
"tz": "UTC",
"points": [
{
"bucket_start": "2025-01-06T00:00:00Z",
"bucket_end": "2025-01-13T00:00:00Z",
"new_affected_count": 3,
"cumulative_affected_count": 12
}
]
}#### Get Affected Tickets
GET /api/v1/findings/{finding_id}/ticketsReturns sample ticket IDs affected by this finding.
Parameters:
| Name | Type | Description |
|---|---|---|
finding_id | path | Finding identifier |
run_id | query | Required. Analysis run identifier |
Response:
{
"finding_id": "uuid",
"finding_type": "F7_PING_PONG",
"finding_type_label": "Ping-Pong-Effekt",
"finding_type_label_en": "Ping-pong effect",
"tickets": [
{
"ticket_id": "INC-2024-0042",
"ticket_id_hash": "5e68376293bc",
"role": "1st Level Support",
"dwell_hours": 76.6,
"effective_dwell_hours": 76.6,
"assigned_at": "2025-12-05T11:09:23",
"outcome": "handover",
"category": "Identity/Access",
"location": "Berlin"
}
],
"total_count": 234,
"returned_count": 20
}Situations
#### List Situations
GET /api/v1/runs/{run_id}/situationsReturns situations (grouped findings) for a run.
Parameters:
| Name | Type | Description |
|---|---|---|
status | query | Filter: new, delegate, watching, ignore, escalated |
tier | query | Filter: critical, notable, observed |
Response:
{
"situations": [
{
"id": "sit-uuid",
"title": "Level-1-Support: Kapazitätsengpass",
"situation_type": "capacity_bottleneck",
"anchor_role": "Level-1-Support",
"impact_hours": 342.5,
"finding_count": 4,
"trend": "worsening",
"status": "new",
"has_narrative": true
}
],
"total": 12,
"run_id": "uuid"
}#### Get Situation Detail
GET /api/v1/situations/{situation_id}Returns full situation details including member findings, delegations, and learnings.
**Concentration block (konzentration, optional):** where a situation is
concentrated across a hidden dimension. Present only when the situation has a
measurable concentration point; absent otherwise (no placeholder).
{
"konzentration": {
"titel_de": "Konzentration",
"achse": "location",
"achse_label_de": "Standorte",
"zeile_pointe_de": "Bei IMS Operations konzentriert sich die Bearbeitungsverzögerung auf 6 von 32 geprüften Standorten (81 % der abgeschlossenen Durchläufe geprüft).",
"zeile_kontrast_de": "Am stärksten: Karlsruhe (3,9×) · Metzingen (2,2×).",
"zeile_entlastung_de": "26 Standorte mit 1.588 Tickets liegen im Normalband; 96 weitere sind mit unter 20 Tickets nicht bewertbar.",
"k_auffaellig": 6,
"n_geprueft": 32,
"n_im_band": 26,
"tickets_im_band": 1588,
"n_nicht_bewertbar": 96,
"ticket_deckung_geprueft": 0.812,
"deckung_ok": true,
"top_werte": [
{"wert": "Karlsruhe", "faktor": 3.87, "faktor_deckel": false, "faktor_label_de": "3,9×", "tier": "critical"}
]
}
}Field notes:
k_auffaellig / n_geprueft: how many values of the anchor role'spopulation show a critical or notable finding, out of those with enough
volume to be assessed (at least 20 tickets). k is always a subset of n.
n_im_band / tickets_im_band: values that were assessed and stay withinthe normal band. Values below the volume threshold are never counted here;
they are reported separately as n_nicht_bewertbar.
ticket_deckung_geprueft: share of completed role passes covered by the assessed values. Below 0.75 the relief sentence is omitted (deckung_ok
is false), while the concentration statement itself remains.
top_werte[].faktor: deviation factor of the strongest values. null means the value has no measurable time-based finding: it still counts towards k
but is rendered without a number. faktor_deckel marks display capping.
zeile_*_de fields are the rendered German sentences; the numericfields carry the same values for your own formatting.
**Origin block (herkunft, optional):** where the waiting time accumulating
at the situation's anchor role comes from. Decomposes the role's handover
waiting time across its inbound edges and reports predecessor roles whose
share of the waiting time is disproportionate to their share of handed-over
tickets. Present only when at least one inbound edge is disproportionate;
absent otherwise (no placeholder). The same block is served on the evidence
poster endpoint with identical values.
{
"herkunft": {
"titel_de": "Herkunft",
"untertitel_de": "Übergaben",
"anker_rolle": "IMS Finance",
"zeile_pointe_de": "Bei IMS Finance trägt die Übergabe von WMD REB Support das 5,0-Fache ihres Ticket-Anteils an Wartezeit (4 % der übergebenen Tickets, 19 % der Übergabe-Wartezeit; Gesamtzeitraum der abgeschlossenen Durchläufe).",
"zeile_hauptzulieferer_de": "Der Hauptzulieferer IT Comline liefert 88 % der übergebenen Tickets und trägt 49 % der Übergabe-Wartezeit (unauffällig).",
"zeile_erstkontakt_de": null,
"signal_kanten": [
{"vorgaenger": "WMD REB Support", "tickets": 48, "ticket_anteil": 0.0382, "wartezeit_anteil": 0.1922, "faktor": 5.04, "faktor_deckel": false}
],
"kanten_im_satz": 1,
"hauptzulieferer": {"vorgaenger": "IT Comline", "tickets": 1113, "ticket_anteil": 0.8847, "wartezeit_anteil": 0.4912, "faktor": 0.56},
"erstkontakt_wartezeit_anteil": 0.2377,
"uebergabe_wartezeit_anteil": 0.7623
}
}Field notes:
signal_kanten: inbound edges whose waiting-time share is at least twicetheir ticket share (with at least 10 distinct tickets and at least 10 % of
the handover waiting time). Sorted by faktor descending; the rendered
sentence names at most the first kanten_im_satz of them.
faktor: waiting-time share divided by ticket share of the edge, computedon the anchor role's completed handed-over passes over the full data range.
faktor_deckel marks display capping ("mehr als das 20-Fache").
hauptzulieferer: the edge delivering the most tickets, as contrast. Therendered sentence appears only when this edge itself is inconspicuous.
erstkontakt_wartezeit_anteil: share of the role's total waiting timethat arises without any handover (first contact) and is therefore not part
of the decomposition. Rendered as a separate honesty line from 0.25 upward.
zeile_*_de fields are the rendered German sentences; the numericfields carry the same values for your own formatting.
(statement.owner_question_slot) additionally carries herkunft_frage_de,
a clarification question addressed to both sides of the strongest edge.
#### Get Situation Narrative
GET /api/v1/situations/{situation_id}/narrativeReturns the AI-generated narrative for a situation: a plain-language explanation with context and recommended actions.
Response:
{
"id": "uuid",
"situation_id": "sit-uuid",
"language": "de",
"headline": "Level-1-Support: Kapazitätsengpass mit steigender Tendenz",
"body": "Der Level-1-Support zeigt seit 8 Wochen eine kontinuierlich steigende Verweildauer...",
"next_actions": [
"Routing-Regeln für Kategorie 'Passwort-Reset' prüfen",
"Kapazitätsplanung für KW 15-20 anpassen"
],
"status": "completed",
"generated_at": "2026-03-01T12:00:00Z"
}#### Get Situation Headline Phrasings
GET /api/v1/situations/{situation_id}/leitzahlReturns every available phrasing of a situation's headline figure. Each phrasing
carries three parts: a compact wert_text for the tile, a short label_de
naming the measure and window, and a full satz_de for surfaces with room.
Also returns the concentration curve (which share of cases carries which share
of the delay) and the time share per case.
All values derive from the same per-case list that produces the situation's
12-week magnitude, so switching the phrasing never changes the number. The share
uses the same 12-week window as the numerator.
Parameters:
| Name | Type | Description |
|---|---|---|
variante | query | Override the leading phrasing for comparison (optional) |
Response:
{
"situation_id": "sit-uuid",
"leitzahl": {
"variante": "anteil",
"wert_text": "88 %",
"label_de": "der gebundenen Verzögerungszeit (12 Wochen)",
"satz_de": "Diese Situation bindet 88 % der gesamten Verzögerungszeit der letzten zwölf Wochen.",
"n_vorgaenge": 311,
"stunden_gesamt": 146447.9,
"euro_gesamt": 7322397,
"anteil_am_lauf": 0.8846,
"median_stunden": 122.0,
"mittelwert_stunden": 470.9,
"kurve": [
{"vorgangs_anteil": 0.1, "n_vorgaenge": 31, "zeit_anteil": 0.491, "stunden": 71852.0}
],
"zeit_anteil_je_vorgang": [
{"vorgang": "ad27acedaa95", "vorgang_nummer": "TCK00030663", "stunden": 3534.0, "zeit_anteil": 0.0241}
],
"varianten": {
"anteil": {"wert_text": "88 %", "label_de": "der gebundenen Verzögerungszeit (12 Wochen)", "satz_de": "..."},
"euro": {"wert_text": "7,3 Mio €", "label_de": "Verzögerungskosten (12 Wochen)", "satz_de": "..."},
"stunden": {"wert_text": "146 Tsd h", "label_de": "über der Norm, 311 Vorgänge (12 Wochen)", "satz_de": "..."},
"typisch": {"wert_text": "5,1 Tage", "label_de": "über der Norm je Vorgang, typisch (12 Wochen)", "satz_de": "..."},
"konzentration": {"wert_text": "49 %", "label_de": "der Zeit aus 10 % der Vorgänge (31 von 311)", "satz_de": "..."}
}
}
}The anteil phrasing is absent when no denominator is available. No percentage is invented.
leitzahl is null when the situation has no evidence inside the 12-week
window. That is an honest empty case rather than a zero sentence.
Triage Queue
GET /api/v1/runs/{run_id}/situations-queueReturns the prioritized triage queue combining situations and ungrouped findings.
Parameters:
| Name | Type | Description |
|---|---|---|
tier | query | Filter by severity |
status | query | Filter by workflow status |
limit | query | Max items (default: 20) |
Delegations
#### Delegate Situation
POST /api/v1/situations/{situation_id}/delegateAssigns responsibility for a situation to a team member via email. The recipient receives a magic-link email with context and guiding questions. Recipients need no login.
Request Body:
{
"email": "[email protected]",
"name": "Max Mustermann",
"message": "Bitte prüfe die Routing-Regeln für Passwort-Reset Tickets.",
"due_date": "2026-03-15"
}Response (201):
{
"delegation_id": "uuid",
"status": "sent",
"feedback_url": "https://app.dkyra.com/feedback/abc123"
}#### List Delegations
GET /api/v1/delegationsReturns all delegations for the current tenant with status tracking.
System Health
#### Get Health Score
GET /api/v1/system-flow/health-scoreReturns the overall system health score for a run.
Parameters:
| Name | Type | Description |
|---|---|---|
run_id | query | Analysis run ID |
Response:
{
"score": 62,
"grade": "C",
"components": {
"flow_balance": 55,
"stock_trend": 70,
"capacity_balance": 48,
"throughput_trend": 75
},
"worst_roles": ["Level-1-Support", "Incident-Management"],
"improving_roles": ["Change-Management"]
}#### Get Role Assessments
GET /api/v1/role-assessmentsReturns pre-computed per-role health assessments for a run.
Parameters:
| Name | Type | Description |
|---|---|---|
run_id | query | Analysis run ID |
severity | query | Filter: critical, notable, observed |
#### Get Role Field
GET /api/v1/runs/{run_id}/role-fieldReturns every role of a run as one point: number of intervals, dwell
distribution (p25/p50/p75/p90), concentration of waiting time, neighbouring
roles above a handover threshold, and the finding profile of that role.
Requires a Pro subscription.
The dwell basis switches the axis, the distribution and the concentration. It
does not change the finding counts: each detector measures on its own clock.
run_at is when the analysis ran, as an ISO-8601 timestamp with an explicit UTC
offset; run_at_source says whether that is the completion (finished) or the
start (created) of the run. Both are null if the run carries neither.
observation is the span the underlying data covers and ends earlier. Its two
timestamps are wall-clock values taken from the source data and carry no zone.
Display them as they are, do not convert them. Only run_at is a machine
timestamp and therefore zone-aware.
Parameters:
| Name | Type | Description |
|---|---|---|
run_id | path | Analysis run ID |
dwell | query | Dwell basis: raw (default) or business |
min_edge | query | Minimum handovers for an edge to be listed (default 15) |
#### Get Contrasts
GET /api/v1/runs/{run_id}/kontrasteReturns the comparisons of a run: inside the area that carries most of the
working time, which groups take markedly longer than is usual there. Everything
the briefing page draws comes from this one call; the client computes nothing.
Each comparison carries both numbers side by side: the added hours and how many
times longer, plus the usual duration they are measured against, so a reader can
redo the arithmetic. Added hours can be negative, and the multiple is null when
the usual duration is too close to zero for a ratio to mean anything. Both are
returned as they are, never clipped or dropped.
geschwiegen lists what the run could not say anything about, and why.
befund is the verdict for the data space as a whole and separates "nothing
stands out" from "nothing can be measured here". Both are null when the run
never computed them. That is a third state, not an empty result.
Percentiles are given per group and are absent for groups below the minimum size
or beyond the cap; the cap travels with the response, so a reader can tell a
complete list from a trimmed one.
Parameters:
| Name | Type | Description |
|---|---|---|
run_id | path | Analysis run ID |
sortierung | query | Order: zusatzzeit (added hours, default) or faktor |
#### Get Evidence For The Leading Comparison
GET /api/v1/runs/{run_id}/briefing-belegeReturns what the leading comparison is made of: the individual durations behind
it, and what the longest cases have in common. Three statements in this order:
how strongly the time clusters, which attribute the clustered cases share, and
who that attribute belongs to.
Drivers are only reported among cases built the same way, that is, sections that
ended by being resolved. Without that restriction the result would describe how
the data is constructed rather than what happened: a section that ends in a
handover is cut short by definition and can never grow long.
A driver that never occurs outside the top tenth carries no multiple at all.
hebel is null and the sentence says "all N cases of this kind are up here"
instead. A ratio without a denominator is not a large number, it is no number.
punkte carries every case of the stated population. When there are too many to
draw, the response says so in punkte_info: all top-tenth cases plus every
n-th of the rest, with schritt naming n. A silent sample never happens.
grund is set, and everything else empty, when the method has nothing to say:
too few cases to search, or the dimension missing from the run's artefact. That
is an answer, not an error, and it arrives with 200.
Parameters:
| Name | Type | Description |
|---|---|---|
run_id | path | Analysis run ID |
#### Get Stellen
GET /api/v1/runs/{run_id}/stellenReturns every stelle of a run exactly once, grouped by one attribute. A stelle is one value of the
chosen attribute: one role, one category, one location, or one origin. Each stelle carries a cell
per finding column with its own count; columns are never summed or ranked against each other,
since they measure in different units.
Stellen already named on the leading briefing page come first (largest share of lost time first),
then the rest, each group ordered by finding count. im_briefing marks that on each stelle;
groesster_anteil marks the single stelle that carries the run.
Each finding entry carries a plain-language label (type_label_de) and headline
(kernaussage_short) instead of the internal type code; both are null when the label registry
has none for that type. A finding whose type maps to no column still counts toward the stelle's
total, listed under deckung.ohne_spalte by its type. Situations are listed per stelle in order of
twelve-week impact; the client resolves their display label from the situation list it already has.
Parameters:
| Name | Type | Description |
|---|---|---|
run_id | path | Analysis run ID |
merkmal | query | Attribute: role (default), category, location, or predecessor_role |
Response:
{
"run_id": "uuid",
"merkmal": "role",
"merkmal_wort": "Rolle",
"spalten": [
{ "id": "laenger", "titel": "Dauert länger", "sortierung": "Wievielfaches des Üblichen" }
],
"stellen": [
{
"merkmal": "role",
"wert": "IMS Operations",
"im_briefing": true,
"groesster_anteil": true,
"befunde": 20,
"situationen": [
{ "id": "uuid", "title": "IMS Operations: Kapazitätsengpass", "impact_hours_12w": 66835.0, "ticket_count": 4622, "finding_count": 37 }
],
"zellen": {
"laenger": {
"n": 5,
"kritisch": 0,
"eintraege": [
{
"snapshot_id": "uuid", "definition_id": "uuid",
"type_label_de": "Prozess-Engpass", "kernaussage_short": "IMS braucht länger",
"tier": "notable", "deviation_factor": 10.7, "ticket_count": 32,
"time_impact_hours": null, "friction_impact_count": null, "trend": "stable",
"weitere_merkmale": [{ "merkmal": "predecessor_role", "wort": "Herkunft", "wert": "KC-Logistik" }],
"situation_id": "uuid"
}
]
}
}
}
],
"deckung": {
"befunde_scharf": 127, "befunde_mit_stelle": 102, "stellen": 27, "stellen_im_briefing": 11,
"ohne_spalte": {}, "unbekannte_typen": []
}
}Building-Block Briefing
#### Get The Building-Block Briefing
GET /api/v1/runs/{run_id}/bausteine-briefingReturns one page in one payload: the same measures the leading briefing uses,
arranged as a chain instead of side by side. Each block consumes the result of
the one before it: which area carries the run, who carries that area, where it
stalls, how it moved week by week, and what can be handed on.
Every number arrives with the population it was measured on and the window it
was measured in. Nothing here is ranked against anything from another block:
hours, counts and rates are different units, and a ranking across them would be
an arithmetic that has no meaning.
traeger says who carries the area, and in which of three modes the page
speaks: one value leads, no value leads, or the baseline is so close to zero
that leading is not a claim anyone can make. The third case is checked first.
A baseline near zero measures the denominator, not the difference.
schwellen carries the thresholds the answer was gated with, so a reader can
see why a block stayed silent instead of guessing. The thresholds are set
values, not derived ones, and they are the same for every data space.
entwicklung reports the weekly series computed from this run's own sections,
on the same population and window as the rest of the page. Weeks at either edge
of the window are marked, because a week cut off by the end of the data is not a
week with less work in it.
kandidaten holds the shortlists behind each block. Anything already handed on
is filtered out server-side, so the next candidate moves up on its own rather
than being remembered by the caller.
adressen_werden_gemerkt says whether an address entered on this page will
still be there next time. Programmatic access has no person to file an entry
under, so nothing is kept then, and the page can say so before anyone types.
ausserhalb counts the sharp findings that no block of this page mentions.
A page that only ever describes what it already shows would report its own
shape.
grund is set, and the blocks carry neutral defaults, when the method has
nothing to say for this run. That is an answer, not an error, and it arrives
with 200. A run that does not exist, or whose underlying data file is gone,
returns 404.
Parameters:
| Name | Type | Description |
|---|---|---|
run_id | path | Analysis run ID |
#### Hand One Finding On
POST /api/v1/runs/{run_id}/bausteine-briefing/anstossRecords that one finding of the building-block briefing has been handed to
someone, together with the text as it stood on the page: the fact, the question,
the lever, and the run it came from. The snapshot is deliberate. The numbers of
the next run are different numbers, and the question was asked about these.
The entry belongs to the data space, not to the run. A question that is open
stays open even when the run it came from is gone, and deleting that run never
removes it.
Handing the same candidate on twice is a conflict, not a second entry: the call
returns 409 and the first entry stays exactly as it was. That same uniqueness
is what makes the next candidate move up on its own. The read endpoint filters
everything already handed on out of its shortlists, so nothing has to be
remembered by the caller.
identity_key is the key the read endpoint delivered for that candidate. Take
it from the answer rather than composing it: two places forming the same key
drift apart. gruppe is the shortlist it came from: typisch, extrem,
unterwegs or weitere. empfaenger_text names a role or a place, not a
person.
A hand-over usually names several places at once: the subject and the
counterparts that work its longest cases. Send one entry per place in
stellen, each with its own address or explicitly held back. One address
cannot stand for several places, so stellen and the older single
empfaenger_email are rejected together with 422.
A message goes out for every place that carries an address, with the fact, the
question and who sent it. Each message also names the other places that were
written to and the ones nobody had an address for, so both sides see the same
distribution. No message carries a link back or a token: the recipient answers
a person, not a system. If sending is not configured or fails, the hand-over is
still recorded and mail_versendet is false with a reason. That is a result,
not an error.
When every place is held back, nur_vermerkt is true: the hand-over is
recorded as a note and nothing is sent. That is a normal outcome, not a
rejected call.
An address entered here is kept for next time, filed under the trait and value
of its place, and comes back pre-filled on the next hand-over naming the same
place. gemerkt lists what was kept. Programmatic access has no person to file
an entry under, so nothing is kept then. The briefing says so in advance via
adressen_werden_gemerkt.
Request Body:
{
"identity_key": "extrem:location:Nord",
"gruppe": "extrem",
"empfaenger_text": "Standort Nord and role Dispatch",
"stellen": [
{"dimension": "location", "wert": "Nord", "name": "Standort Nord",
"email": "[email protected]"},
{"dimension": "role", "wert": "Dispatch", "name": "Rolle Dispatch",
"zurueckgestellt": true}
],
"payload": {
"fakt": "Cases from this location wait 3.4x longer",
"frage": "What happens to these cases before they reach you?",
"hebel": 3.4
}
}Response (201):
{
"eintrag": {
"id": "uuid",
"identity_key": "extrem:location:Nord",
"gruppe": "extrem",
"empfaenger_text": "Standort Nord and role Dispatch",
"empfaenger_email": null,
"stellen": [
{"dimension": "location", "wert": "Nord", "name": "Standort Nord",
"email": "[email protected]", "zurueckgestellt": false},
{"dimension": "role", "wert": "Dispatch", "name": "Rolle Dispatch",
"email": null, "zurueckgestellt": true}
],
"status": "delegiert",
"angestossen_am": "2026-09-02T09:00:00",
"payload": {"fakt": "...", "frage": "...", "run_id": "uuid"}
},
"mail_versendet": true,
"mail_grund": null,
"versendet_an": ["Standort Nord"],
"gemerkt": ["Standort Nord"],
"nur_vermerkt": false
}Parameters:
| Name | Type | Description |
|---|---|---|
run_id | path | Analysis run ID |
#### Look Up The Address Of Each Place
POST /api/v1/runs/{run_id}/bausteine-briefing/adressenReturns, in the order asked, the address already on file for each place a
hand-over names: the address, who it is filed under, and when that entry was
last touched. A place with nothing on file comes back with email set to
null. That is an answer, not a gap, so a client can show a visibly empty field
instead of no field at all.
Places are identified by trait and value together. A value like Anwendung can
be a role and a topic at the same time, and those are two different places with
two different addresses.
Whether an address entered now will still be there next time is answered once
for the whole page, by adressen_werden_gemerkt on the briefing itself, so it
can be said before anyone starts typing.
Request Body:
{
"stellen": [
{"dimension": "location", "wert": "Nord"},
{"dimension": "role", "wert": "Dispatch"}
]
}Response (200):
{
"stellen": [
{"dimension": "location", "wert": "Nord", "email": "[email protected]",
"name": "Standort Nord", "zuletzt": "2026-09-02T09:00:00"},
{"dimension": "role", "wert": "Dispatch", "email": null,
"name": null, "zuletzt": null}
]
}Parameters:
| Name | Type | Description |
|---|---|---|
run_id | path | Analysis run ID |
Time In Transit
#### Get Time In Transit
GET /api/v1/runs/{run_id}/unterwegsReturns how much of the waiting time was spent at stations that did not solve
the case: in hours, and as a share of the total waiting time. The share is what
makes two data spaces comparable; the hours are what makes the share concrete.
Counted are only completed cases. The filter matters: it changes the number
substantially, and without it the result measures how the data was cut rather
than how work flowed. Cases that passed through a single station have a value of
zero by construction. That is a statement, not a gap.
stufe says how much room the number deserves: fuehrt when transit time is the
larger story, voll when it is a full one, satz when it is small enough that a
single sentence is the honest form. The thresholds behind it are deliberate
estimates, adjustable, and live in one place.
Drivers name where the time sat, not why. The number of sections is reported
next to the hours because the two together separate a busy way-station from a
parked case: the same share of hours means something different across eight
thousand short sections than across fifty long ones.
ueberschneidung counts how many of these cases also appear among the longest
waiting times overall. The two views are connected through the same cases, never
by adding their numbers together.
A run computed before this measure existed answers 200 with
vorhanden: false and a reason. That is a third state, not an empty result.
Parameters:
| Name | Type | Description |
|---|---|---|
run_id | path | Analysis run ID |
#### Get Time In Transit Over Runs
GET /api/v1/runs/{run_id}/unterwegs/verlaufReturns the same measure across the runs of this data space, oldest first, so a
reader can tell a snapshot from a trend. Each entry carries the run, its date,
the share, the hours, and how much of it the top tenth carried.
Only runs that computed the measure appear. Earlier runs are absent rather than
zero. A missing value and a value of zero are different claims.
Parameters:
| Name | Type | Description |
|---|---|---|
run_id | path | Analysis run ID (identifies the data space) |
Billing & Credits
#### Get AI Credits Status
GET /api/v1/billing/ai-creditsReturns remaining AI analysis credits and current-period usage for the current tenant.
Response:
{
"tenant_id": "uuid",
"tier": "pro",
"credits_total": 100,
"credits_included": 80,
"credits_purchased": 20,
"warning_level": "ok",
"runs_remaining": 77,
"runs_used": 23,
"runs_limit": 100,
"enrichment_cost_eur_today": 0.42,
"enrichment_tokens_today": 12500,
"enrichment_count_today": 3,
"period_start": "2026-06-01T00:00:00Z",
"period_end": "2026-07-01T00:00:00Z"
}Connectors
Process Radar supports direct ITSM integrations for automated data import.
#### List Available Connector Types
GET /api/v1/connector-typesReturns available connector types (e.g., Matrix42, Jira Service Management).
#### Create Connector
POST /api/v1/connectorsCreates a new ITSM connector for automated data synchronization.
#### Test Connection
POST /api/v1/connectors/{connector_id}/testTests connectivity to the ITSM system without importing data.
#### Start Sync
POST /api/v1/connectors/{connector_id}/syncTriggers a data synchronization from the connected ITSM system. Only a full
synchronization is supported ("sync_type": "full"); every sync re-imports the
complete ticket history to guarantee no data is lost.
Authentication
#### Login
POST /api/v1/auth/loginRequest Body:
{
"email": "[email protected]",
"password": "your-password"
}Response:
{
"access_token": "eyJhbGci...",
"refresh_token": "eyJhbGci...",
"token_type": "bearer",
"expires_in": 300,
"user": {
"user_id": "uuid",
"email": "[email protected]",
"role": "owner"
}
}#### Refresh Token
POST /api/v1/auth/refreshRefreshes an expired access token using the refresh token.
#### Current User
GET /api/v1/auth/meReturns the current authenticated user and tenant information.
AI Integration
Process Radar is built for programmatic and AI-agent consumption. All endpoints return structured JSON with consistent schemas.
Recommended Workflows for AI Agents
Morning Briefing:
GET /api/v1/executive-summary: Top situations, health score, trendsContinuous Monitoring:
GET /api/v1/flow-health: Check for new bottlenecksGET /api/v1/analysis-summary: Track anomaly trendsDeep Dive:
GET /api/v1/runs/{id}/findings?tier=critical: Critical anomaliesGET /api/v1/situations/{id}: Situation contextGET /api/v1/situations/{id}/narrative: AI-generated explanationDelegation Automation:
GET /api/v1/runs/{id}/situations-queue: Triage queuePOST /api/v1/situations/{id}/delegate: Assign responsibilityGET /api/v1/delegations: Track follow-upsWhat Process Radar Detects
Process Radar detects various types of anomalies including:
Each finding includes a human-readable label and a severity classification (critical, notable, observed).
Response Conventions
critical, notable, observedimproving, stable, worsening, newhigh, medium, lowstatements.de / statements.enCache-Control: private, max-age=300Error Handling
All errors follow RFC 7807 Problem Details format:
{
"type": "https://dkradar/errors/NOT_FOUND",
"title": "Not Found",
"status": 404,
"detail": "Run not found",
"instance": "/api/v1/runs/unknown-id",
"code": "NOT_FOUND",
"trace_id": "e29f788f-ffbc-4686-be22-4a8e6d422997"
}Validation errors (422) additionally carry a field-level errors list:
{
"type": "https://dkradar/errors/VALIDATION_ERROR",
"title": "Validation Error",
"status": 422,
"detail": "Validation error",
"code": "VALIDATION_ERROR",
"errors": [
{ "loc": ["query", "run_id"], "msg": "Field required", "type": "missing" }
]
}Common Status Codes:
| Status | Meaning |
|---|---|
| 200 | Success |
| 201 | Created |
| 202 | Accepted (async operation started) |
| 204 | Deleted (no content) |
| 400 | Bad request (validation error) |
| 401 | Authentication required |
| 403 | Insufficient permissions or feature not available |
| 404 | Resource not found |
| 409 | Conflict (e.g., run still in progress) |
| 422 | Validation error (detailed field-level errors) |
| 429 | Rate limit exceeded |
| 500 | Internal server error |
Rate Limits
Standard: 200 requests/minute per client. Summary endpoints are cached for 5 minutes.
Higher limits available on Enterprise plans.
Integration Examples
Python
import requests
API_KEY = "your-api-key"
BASE = "https://api.dkyra.com/api/v1"
headers = {"X-API-Key": API_KEY}
# Get executive summary
summary = requests.get(f"{BASE}/executive-summary", headers=headers).json()
print(f"Health: {summary['health_score']['grade']}")
for sit in summary["priority_situations"]:
print(f" [{sit['tier']}] {sit['headline']} ({sit['impact_hours']}h)")JavaScript
const API_KEY = "your-api-key";
const BASE = "https://api.dkyra.com/api/v1";
const res = await fetch(`${BASE}/executive-summary`, {
headers: { "X-API-Key": API_KEY }
});
const summary = await res.json();
console.log(`Health: ${summary.health_score.grade}`);cURL
curl -H "X-API-Key: YOUR_KEY" https://api.dkyra.com/api/v1/executive-summarySupported ITSM Systems
| System | Integration | Status |
|---|---|---|
| Matrix42 | Native connector | Available |
| Jira Service Management | Native connector | Available |
| ServiceNow | Planned | Coming soon |
| TOPdesk | Planned | Coming soon |
| Any ITSM | CSV import | Available |