Process Radar API Documentation

API Version: 1.0.0
Base URL: https://api.dkyra.com/api/v1
Authentication: 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

ConceptDescription
FindingAn 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 DimensionsThe business context of a finding: which team, category, location, or combination is affected
TierSeverity classification: critical, notable, observed
TrendDevelopment over time: improving, worsening, stable, new
ConfidenceRobustness assessment (low, medium, high) based on data volume and statistical significance
SituationA 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 ScoreAn A-through-F grade summarizing overall process health, combining throughput, bottleneck risk, and trend indicators
DelegationAssignment of responsibility with feedback loop via magic-link email. Recipients need no login
NarrativeAI-generated explanation of a situation in plain language with recommended actions
StatementHuman-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-key

JWT Bearer Token

Authorization: Bearer your-jwt-token

Obtaining 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.

Roles

Workspace members have one of four roles: owner, admin, member, viewer. Viewers can read and download everything their workspace can see, but cannot change workspace data (status changes, assessments, delegations, analysis runs, uploads). Such requests return 403 with "code": "READ_ONLY_ROLE". API keys have the rights of a member.


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 /health

Returns API status. No authentication required.

Response:

{
  "status": "ok"
}

Datasets & Upload

#### List Datasets

GET /api/v1/datasets

Returns the datasets for the current tenant, including run counts. The dataset

that uploads and syncs write into comes first: the one with the most recent

successful analysis run (datasets without one follow, newest first). Retired

datasets are not listed; they stay readable via GET /api/v1/datasets/{dataset_id}

and their runs stay readable, but no new analysis can be started on them.

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/upload

Uploads CSV files into the tenant's dataset in one step. Uses multipart/form-data.

A tenant has one dataset: the first upload creates it, every further upload

extends it and does not replace earlier data.

  • Tickets are matched by ticket ID; when a ticket appears again, the new file wins.
  • History rows that are identical to existing rows (dates compared as points in
  • time, regardless of format) are skipped; all other rows are appended.

  • The columns of a new upload must match the dataset's columns. Order and
  • equivalent column names (e.g. Ticket / TicketNumber) may differ; missing or

    additional columns reject the upload with 400, naming the columns, and the

    dataset stays unchanged.

  • The merged files use one date format: YYYY-MM-DD HH:MM:SS.
  • Form Fields:

    NameTypeDescription
    tickets_filefileTickets.csv (semicolon-separated)
    history_filefileTicketHistory.csv (semicolon-separated)
    namestringDataset name (default: "Neue Analyse")

    Response (200):

    {
      "dataset_id": "uuid",
      "status": "UPLOADED",
      "validation": {
        "ok": true,
        "ticket_count": 1800,
        "event_count": 52000,
        "warnings": [],
        "merged": true,
        "delivery": { "ticket_count": 400, "event_count": 9000 },
        "dataset":  { "ticket_count": 1800, "event_count": 52000 }
      }
    }
    FieldMeaning
    validation.deliveryCounts in the uploaded files
    validation.datasetCounts in the dataset after the upload
    validation.ticket_count / event_countSame as validation.dataset
    validation.mergedtrue if the upload was merged into existing data
    validation.warningsIncludes a notice if the dataset's earlier raw data was missing; the upload is then taken as a first upload
    StatusMeaning
    200Uploaded (see validation)
    400Rejected: columns differ from the dataset, unreadable file or dates, or not a CSV file; the dataset is unchanged
    413File larger than 100 MB

    #### Delete Dataset

    DELETE /api/v1/datasets/{dataset_id}?confirm=true

    Deletes a dataset and ALL associated data. Requires confirm=true as safety guard.

    StatusMeaning
    204Successfully deleted
    400Missing confirm=true parameter
    404Dataset not found
    409Cannot delete: active run in progress

    Analysis Runs

    #### Start Analysis Run

    POST /api/v1/datasets/{dataset_id}/runs

    Starts 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"
    }
    StatusMeaning
    202Analysis started
    402Not enough credits
    403Plan limit for datasets exceeded (retired datasets do not count)
    409The dataset is retired; start the analysis on the current dataset

    #### List Runs

    GET /api/v1/runs

    Returns all runs for the current tenant.

    Parameters:

    NameTypeDescription
    dataset_idqueryFilter by dataset
    statusqueryFilter: QUEUED, RUNNING, SUCCEEDED, FAILED
    limitqueryMax results (default: 50)
    offsetqueryPagination 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-summary

    High-level briefing with system health, prioritized situations, and trend information.

    Parameters:

    NameTypeDescription
    dataset_idqueryDataset ID (uses first dataset if omitted)
    limitqueryMax 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-summary

    Aggregated 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-health

    System-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}/findings

    Returns all detected anomalies for an analysis run with filtering and sorting.

    Parameters:

    NameTypeDescription
    run_idpathAnalysis run identifier
    tierqueryFilter: critical, notable, observed
    finding_typequeryFilter by finding type identifier
    rolequeryFilter by affected role
    categoryqueryFilter by affected category
    locationqueryFilter by affected location
    min_impactqueryMinimum impact in hours
    ticket_statusqueryFilter: closed (default), open, all
    workflow_statusqueryFilter: active (default), new, delegate, watching, ignore, escalated, all
    sort_byquerySort: relevance_score (default), impact_hours, tier
    sort_orderqueryDirection: asc or desc
    limitqueryMax results (default: 50, max: 500)
    offsetqueryPagination 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": {}
    }

    Each finding also carries type_explanation, the same static explanation of the finding type that the detail endpoint returns (what_de / what_en, how_to_read_de / how_to_read_en; null for types without an explanation).

    Response fields like finding_type contain machine-readable identifiers. Use statements.de / statements.en for human-readable display. constellation names the affected dimension values; dimensions lists 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}/timeseries

    Returns weekly time series data for a finding (affected tickets per ISO week), useful for sparklines and trend visualization.

    Parameters:

    NameTypeDescription
    finding_idpathFinding identifier
    run_idqueryRequired. Analysis run identifier
    viewquerylast_52_weeks (default) or full_history
    as_ofqueryTime 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}/tickets

    Returns sample ticket IDs affected by this finding.

    Parameters:

    NameTypeDescription
    finding_idpathFinding identifier
    run_idqueryRequired. 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}/situations

    Returns situations (grouped findings) for a run.

    Parameters:

    NameTypeDescription
    statusqueryFilter: new, delegate, watching, ignore, escalated
    tierqueryFilter: 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's
  • population 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 within
  • the 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.

  • The three zeile_*_de fields are the rendered German sentences; the numeric
  • fields 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 names the predecessor role that a

    statistically confirmed origin finding of this situation points to, together

    with that edge's share of tickets and of waiting time. Present only when the

    situation carries such a confirmed finding and the decomposition covers its

    edge; absent otherwise (no placeholder). The same block is served on the

    evidence poster endpoint with identical values. If the named edge's factor is

    below 1.0, zeile_pointe_de names the origin without stating a multiple.

    {
      "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: exactly one entry, the inbound edge named by the confirmed
  • origin finding. It is the edge the rendered sentence names

    (kanten_im_satz is 1). Its faktor can be below 2; the sentence then

    states it as it is.

  • faktor: waiting-time share divided by ticket share of the edge, computed
  • on 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. The
  • rendered sentence appears only when this edge is inconspicuous (faktor

    below 2) and is not itself the named edge.

  • erstkontakt_wartezeit_anteil: share of the role's total waiting time
  • that arises without any handover (first contact) and is therefore not part

    of the decomposition. Rendered as a separate honesty line from 0.25 upward.

  • The zeile_*_de fields are the rendered German sentences; the numeric
  • fields carry the same values for your own formatting.

  • When the block is present, the situation's owner slot
  • (statement.owner_question_slot) additionally carries herkunft_frage_de,

    a clarification question addressed to both sides of the named edge.


    #### Get Situation Narrative

    GET /api/v1/situations/{situation_id}/narrative

    Returns 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}/leitzahl

    Returns 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:

    NameTypeDescription
    variantequeryOverride 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-queue

    Returns the prioritized triage queue combining situations and ungrouped findings.

    Parameters:

    NameTypeDescription
    tierqueryFilter by severity
    statusqueryFilter by workflow status
    limitqueryMax items (default: 20)

    Delegations

    #### Delegate Situation

    POST /api/v1/situations/{situation_id}/delegate

    Assigns 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/delegations

    Returns all delegations for the current tenant with status tracking.


    System Health

    #### Get Health Score

    GET /api/v1/system-flow/health-score

    Returns the overall system health score for a run.

    Parameters:

    NameTypeDescription
    run_idqueryAnalysis 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-assessments

    Returns pre-computed per-role health assessments for a run.

    Parameters:

    NameTypeDescription
    run_idqueryAnalysis run ID
    severityqueryFilter: critical, notable, observed

    #### Get Role Field

    GET /api/v1/runs/{run_id}/role-field

    Returns 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.

    Each listed finding carries menge_zahl and menge_wort: the count the finding

    refers to and the unit of that count. For most findings this is the number of

    affected tickets. ticket_count is kept as a sorting value and can be larger,

    for example where a finding reports a rate over a wider base.

    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:

    NameTypeDescription
    run_idpathAnalysis run ID
    dwellqueryDwell basis: raw (default) or business
    min_edgequeryMinimum handovers for an edge to be listed (default 15)

    #### Get Contrasts

    GET /api/v1/runs/{run_id}/kontraste

    Returns 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:

    NameTypeDescription
    run_idpathAnalysis run ID
    sortierungqueryOrder: zusatzzeit (added hours, default) or faktor

    #### Get Evidence For The Leading Comparison

    GET /api/v1/runs/{run_id}/briefing-belege

    Returns 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:

    NameTypeDescription
    run_idpathAnalysis run ID

    #### Get Stellen

    GET /api/v1/runs/{run_id}/stellen

    Returns 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, one origin, or one organisational unit. 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.

    The response also carries aussage: statements about the whole attribute, one row per comparison

    group (for example "within role X, locations differ from A to B"). Each row names its comparison

    group, its range, two counts and its slow and fast end; kennung identifies the comparison it rose

    from. im_briefing marks the one row the briefing page shows as its typical case; that row is

    listed first. wiederholte_enden lists each value that is the slow end in at least two rows,

    counted separately per kind of comparison group (als_langsames_ende of zeilen). These counts are

    never added across kinds: groups of different kinds can hold the same tickets. When no row exists,

    grund says why: for role, because a role is always the comparison group, never the attribute

    that is compared. ou (organisational unit) is accepted as attribute; findings carry no

    organisational unit, so its grid stays empty, raster_grund says so, and aussage carries its rows.

    Parameters:

    NameTypeDescription
    run_idpathAnalysis run ID
    merkmalqueryAttribute: role (default), category, location, predecessor_role, or ou

    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-briefing

    Returns 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.

    typisch is the one comparison the page is about, and every other block on

    the page follows it: the point cloud in wolke and the counterparts in

    kandidaten.extrem look at the same area rather than choosing their own.

    typisch.bestaetigung says what the comparison rests on: whether the spread

    across all groups exceeds chance (achsenband), and how many single values

    were confirmed on their own out of how many tested (bestaetigt, geprueft),

    with their names (werte) and the period that test ran on. The two tests run

    on different periods and answer different questions, so they are reported

    side by side and never combined. schlecht_bestaetigt says whether the slow

    end shown is itself one of the confirmed values. When the usual time of a

    comparison is below half an hour, unter_sockel is true and faktor is

    null: a ratio against a baseline near zero measures the denominator, so only

    the two measured ends are given. Such a comparison can still lead the page.

    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.

    The bundled findings in kandidaten.vorgang are sorted into four baskets, one

    for each unit they are measured in: time, friction, time of day and outcome.

    korb names the basket; elle and elle_wert name the measure the basket is

    ordered by: inside the time basket the share of that basket's lost time, in the

    others the number of cases affected in the window. The list runs basket by

    basket in that fixed order and is ordered only inside a basket, never across

    baskets. Findings already marked as seen are left out, and no entry carries an

    hour total. in_station3 names the blocks further up the page that already show

    the same place, so it need not appear twice. deckung.ohne_korb lists anything

    that fits no basket; it is meant to stay empty.

    Every entry in kandidaten.vorgang also carries befunde: the individual

    findings behind it, grouped by their own basket, anchor finding first, then

    the result of the chance test (confirmed before untested before not

    confirmed), then severity tier, then case count. Each finding carries that

    result as zufall_geprueft (geprueft, nicht_bestaetigt, nicht_pruefbar,

    or null for runs from before the test existed), so a page can tell a

    confirmed finding from an unconfirmed one instead of ordering both by size. len(befunde) equals the entry's own

    findings count exactly -- every member is counted, whether or not it carries

    a time-window impact. No finding here carries an hour total either; a case

    count is the only currency-free measure that spans all finding types.

    Each finding in befunde can also carry mass_satz: a ready-made sentence

    built from that finding's own stored measurements, one wording per finding

    type. It is null for a finding type with no wording of its own -- a shared

    wording across different measures would misstate at least one of them.

    zeitpunkt reports the finding's raw cyclical coordinate (a weekday, a

    time-of-day bucket, or a weekend flag) when it has one, untranslated; null

    when it has none. thema names the finding's topic (speed, distance, timing

    or outcome), read directly from the finding type's registry entry -- null

    only for a type outside that registry. It is independent of korb: a

    finding type with no unit of measurement can still carry a topic here.

    Each entry in kandidaten.vorgang can also carry uebergaenge: the busiest

    counterpart roles handing cases into, and taking cases out of, its anchor

    role, up to three per direction, together with the total case count in each

    direction (counting every transition the run recorded, not only the three

    shown). null when the entry has no anchor role, or the underlying signal

    has nothing for this run.

    Candidates in kandidaten.weitere carry definition_id, the stable reference

    used to accept one of them (see below), never the text key identity_key,

    which is not unique for these entries.

    A candidate that has been accepted as a known trade-off is filtered out of

    kandidaten.vorgang / kandidaten.weitere and listed in angenommen[]

    instead, together with the yardstick recorded at acceptance and the two

    thresholds derived from it. A candidate that a run just sent back, because the

    direction turned for the worse or its height or width outgrew that yardstick,

    stays a plain card but carries rueckkehr: the reasons, the yardstick, and

    the values measured now. It carries rueckkehr only in the one run it came

    back; a run later it is an ordinary candidate again.

    Every candidate in kandidaten.vorgang and kandidaten.weitere carries

    achsenwert_fehlt, true when it has no value above zero on its basket's axis

    (elle_wert missing or not above zero for a bundled entry; always true for a

    single finding, which carries no basket width of its own). A single finding whose underlying

    definition already belongs to a bundled entry of this run is left out of

    kandidaten.weitere altogether and named instead in

    deckung.einzelbefund_in_eintrag, together with the entry it belongs to and

    whether it is that entry's anchor. It is reachable only through that entry,

    never as a finding of its own. The one exception is a single finding this same run just sent back

    (see rueckkehr above): it stays a candidate even when its definition also

    belongs to a bundled entry, so a returned trade-off is never lost from view.

    A bundled entry at the lowest severity tier is left out of kandidaten.vorgang

    and named instead in deckung.beobachtet_ausgeblendet, unless its direction

    has turned for the worse on a signal reliable enough to act on, or its width

    in the last 12 weeks is more than 20 percent above its width in the previous

    run. An entry this same run just sent back from an accepted trade-off is

    never left out on this basis, whatever its severity. This rule leaves alone

    an entry that is already excluded from the page for another reason.

    Each hand-over in kandidaten.unterwegs carries anteil_unterwegs: the share

    of hand-over time in completed cases that this hand-over accounts for, counted

    on the same completed cases as its hours and its case count.

    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.

    uebergabe_moeglich says whether this access may hand a finding on at all:

    handing on needs a signed-in person, so it is false for programmatic access.

    angestossen lists what has been handed on, newest first. Each entry carries

    one place per delegation with its own state and deadline, plus the places that

    were held back. Its status is delegiert while answers are outstanding,

    ueberfaellig once an open place is past its deadline, and beantwortet as

    soon as one place has answered.

    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:

    NameTypeDescription
    run_idpathAnalysis run ID

    #### Hand One Finding On

    POST /api/v1/runs/{run_id}/bausteine-briefing/anstoss

    Hands one finding of the building-block briefing on to someone. Every place

    with an address becomes its own delegation, with the same reply link, deadline

    and state as any other delegation in the app, and it appears in

    GET /api/v1/delegations. Each delegation records 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.

    Handing on requires a signed-in person, because a delegation carries its

    sender. Programmatic access receives 403 before anything is written or sent;

    the briefing says so in advance via uebergabe_moeglich. A hand-over needs at

    least one address: when every place is held back, the call returns 422 and

    nothing is recorded.

    The delegations belong 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 nothing changes. That same rule 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. Once every delegation of a hand-over has been cancelled, the candidate

    returns to its list and can be handed on again.

    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, who sent it, the deadline if one was set (due_date, YYYY-MM-DD),

    an optional personal note (nachricht), and a link through which the

    recipient answers without signing in. 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. stellen[].person_name optionally names the person

    behind an address. 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. nur_vermerkt is always false and kept for older clients.

    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]", "person_name": "Alex Doe"},
        {"dimension": "role", "wert": "Dispatch", "name": "Rolle Dispatch",
         "zurueckgestellt": true}
      ],
      "due_date": "2026-10-15",
      "nachricht": "Could you take a look before the review?",
      "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,
           "status": "sent", "due_date": "2026-10-15", "beantwortet_am": null},
          {"dimension": "role", "wert": "Dispatch", "name": "Rolle Dispatch",
           "email": null, "zurueckgestellt": true,
           "status": null, "due_date": null, "beantwortet_am": null}
        ],
        "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:

    NameTypeDescription
    run_idpathAnalysis run ID

    #### Accept A Candidate As A Known Trade-Off

    POST /api/v1/runs/{run_id}/bausteine-briefing/annahme

    Accepts one candidate of the building-block briefing, a bundled finding or a

    single finding, as a known trade-off. Accepting sets a yardstick, measured

    from the run behind this page: height, width, and direction, whichever apply.

    The candidate leaves its basket until a later run finds it got worse: the

    direction turns for the worse, or its height or width grows past the accepted

    yardstick by more than the app's fixed margin. That comparison is made by the

    run that writes the next answer, not by this call.

    art is vorgang for a bundled finding or befund for a single finding.

    bezug_id is the reference the read endpoint delivered for that candidate:

    id for a bundled finding, definition_id for a single finding, never a text

    key.

    A run that does not exist, or belongs to a different data space, or does not

    currently hold this candidate, returns 404. A candidate the run cannot place

    in any basket returns 409, and so does accepting an already-accepted

    candidate again. Accepting the same candidate again after it came back

    replaces the earlier yardstick with a fresh one.

    Request Body:

    {
      "art": "vorgang",
      "bezug_id": "uuid"
    }

    Response (201):

    {
      "id": "uuid",
      "art": "vorgang",
      "bezug_id": "uuid",
      "status": "angenommen",
      "massstab": {"hoehe": 3.2, "breite": 40, "richtung": "nicht_schlechter", "richtung_belastbar": true}
    }

    Parameters:

    NameTypeDescription
    run_idpathAnalysis run ID

    #### Undo Accepting A Candidate

    POST /api/v1/bausteine-briefing/annahmen/{annahme_id}/zuruecknehmen

    Undoes an accept: the candidate is a candidate again on the next read. The row

    is deleted, not kept as history: a stale yardstick would otherwise still be

    measured against on the next run. An accept the caller's data space does not

    own returns 404.

    Response (200):

    {
      "id": "uuid",
      "geloescht": true
    }

    Parameters:

    NameTypeDescription
    annahme_idpathID of the accepted-candidate entry

    #### Look Up The Address Of Each Place

    POST /api/v1/runs/{run_id}/bausteine-briefing/adressen

    Returns, 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:

    NameTypeDescription
    run_idpathAnalysis run ID

    Time In Transit

    #### Get Time In Transit

    GET /api/v1/runs/{run_id}/unterwegs

    Returns 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:

    NameTypeDescription
    run_idpathAnalysis run ID

    #### Get Time In Transit Over Runs

    GET /api/v1/runs/{run_id}/unterwegs/verlauf

    Returns 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:

    NameTypeDescription
    run_idpathAnalysis run ID (identifies the data space)

    Billing & Credits

    #### Get AI Credits Status

    GET /api/v1/billing/ai-credits

    Returns 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-types

    Returns available connector types (e.g., Matrix42, Jira Service Management).


    #### Create Connector

    POST /api/v1/connectors

    Creates a new ITSM connector for automated data synchronization.


    #### Test Connection

    POST /api/v1/connectors/{connector_id}/test

    Tests connectivity to the ITSM system without importing data.


    #### Start Sync

    POST /api/v1/connectors/{connector_id}/sync

    Triggers 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.

    A sync replaces the dataset completely. If CSV uploads have been merged into the

    dataset, the sync is refused with 409 so that this data is not lost. A dataset

    filled by a single CSV upload (without a merge) can still be switched to a connector.


    Authentication

    #### Login

    POST /api/v1/auth/login

    Request 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/refresh

    Refreshes an expired access token using the refresh token.


    #### Current User

    GET /api/v1/auth/me

    Returns 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, trends
  • Present to user with recommended actions
  • Continuous Monitoring:

  • GET /api/v1/flow-health: Check for new bottlenecks
  • GET /api/v1/analysis-summary: Track anomaly trends
  • Alert user when critical findings appear
  • Deep Dive:

  • GET /api/v1/runs/{id}/findings?tier=critical: Critical anomalies
  • GET /api/v1/situations/{id}: Situation context
  • GET /api/v1/situations/{id}/narrative: AI-generated explanation
  • Delegation Automation:

  • GET /api/v1/runs/{id}/situations-queue: Triage queue
  • POST /api/v1/situations/{id}/delegate: Assign responsibility
  • GET /api/v1/delegations: Track follow-ups
  • What Process Radar Detects

    Process Radar detects various types of anomalies including:

  • Dwell Time Anomalies: Teams or categories where tickets take significantly longer than expected
  • Routing Loops: Tickets bouncing between teams without resolution
  • Capacity Bottlenecks: Teams where incoming work exceeds processing capacity
  • Cascade Effects: Problems in one team causing delays in downstream teams
  • Seasonal Patterns: Time-dependent fluctuations in ticket processing
  • Wait Time Dominance: Processes where most time is spent waiting, not working
  • Each finding includes a human-readable label and a severity classification (critical, notable, observed).

    Response Conventions

  • Severity tiers: critical, notable, observed
  • Trends: improving, stable, worsening, new
  • Confidence levels: high, medium, low
  • Bilingual content: statements.de / statements.en
  • Timestamps: ISO 8601 format
  • Cache: Summary endpoints return Cache-Control: private, max-age=300

  • Error 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:

    StatusMeaning
    200Success
    201Created
    202Accepted (async operation started)
    204Deleted (no content)
    400Bad request (validation error)
    401Authentication required
    403Insufficient permissions or feature not available
    404Resource not found
    409Conflict (e.g., run still in progress)
    422Validation error (detailed field-level errors)
    429Rate limit exceeded
    500Internal 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-summary

    Supported ITSM Systems

    SystemIntegrationStatus
    Matrix42Native connectorAvailable
    Jira Service ManagementNative connectorAvailable
    ServiceNowPlannedComing soon
    TOPdeskPlannedComing soon
    Any ITSMCSV importAvailable

    Support

  • Documentation: https://dkyra.com/docs
  • API Support: [email protected]
  • Website: https://datakyra.com