---
title: "Meldefälle (NIS-2)"
description: "Einen NIS-2-Meldefall führen: Kriterien, Entscheidung zur Meldepflicht, Zeitpunkt der Kenntnis, Fristen, Berichtsentwürfe und das Protokoll Ihrer Abgaben im BSI-Portal."
---

`GET /api/im/report-cases` · `POST /api/im/report-cases` · `GET /api/im/report-cases/:id` · `PATCH /api/im/report-cases/:id`

`POST /api/im/report-cases/:id/aware-at` · `POST /api/im/report-cases/:id/criteria` · `POST /api/im/report-cases/:id/decision` · `POST /api/im/report-cases/:id/interim-request` · `POST /api/im/report-cases/:id/close`

`POST /api/im/report-cases/:id/incidents` · `DELETE /api/im/report-cases/:id/incidents/:incidentId` · `GET /api/im/report-cases/:id/draft` · `POST /api/im/report-cases/:id/submissions`

Ein Meldefall sammelt, was Sie für eine NIS-2-Meldung brauchen (Art. 23 NIS-2-Richtlinie, § 32 BSIG): die verknüpften Vorfälle, je Erheblichkeitskriterium eine Entscheidung, die Entscheidung zur Meldepflicht, den Zeitpunkt der Kenntnis und die Fristen daraus (Frühwarnung 24 h, Meldung 72 h, Abschlussmeldung einen Monat nach der Meldung, Zwischenmeldung auf Anfrage). **Jede Meldung geben Sie selbst im BSI-Portal ab. Uptimeify übermittelt nichts und stuft keinen Vorfall selbst ein.** Kriterien, Abgaben und Zeitachse sind nur anfügbar: nachträglich lässt sich nichts ändern oder löschen. Geschlossene Fälle werden drei Jahre aufbewahrt.

`:id` ist die `publicId` des Falls oder seine numerische `id`. `:incidentId` ist die numerische ID eines IM-Vorfalls.

## Authentifizierung

Nur mit Sitzung (Dashboard-Anmeldung); API-Tokens werden mit `403` (`nis2OfficerRequired`) abgewiesen. NIS-2-Meldefälle müssen eingeschaltet sein ([NIS-2-Einstellungen](/de/api/incident-management/nis2-settings)). Organisations-Admins und eingetragene Meldeverantwortliche dürfen lesen und schreiben, alle anderen bekommen `403`. Ein Fall einer anderen Organisation antwortet `404` (`reportCaseNotFound`), genau wie ein fehlender.

## Meldefälle auflisten

`GET /api/im/report-cases`

| Query | Beschreibung |
|-------|--------------|
| `status` | Kommagetrennter Statusfilter, z. B. `reportable,early_warning_submitted`. |
| `page` | Standard `1`. |
| `perPage` | Standard `50`, höchstens `200`. |

Sortiert nach der nächsten offenen Frist; Fälle ohne Frist stehen am Ende, neueste zuerst.

```bash
curl "$BASE_URL/api/im/report-cases?status=reportable&perPage=20" -H "Cookie: $SESSION_COOKIE"
```

`200 OK`

```json
{
  "items": [
    {
      "id": 12,
      "publicId": "0b6f9a52-7a43-4c8e-9f1e-5c2d8a1b3e77",
      "title": "Ausfall Kundenportal",
      "status": "reportable",
      "awareAt": "2026-10-21T07:00:00.000Z",
      "createdAt": "2026-10-21T07:05:00.000Z",
      "nextDeadline": { "stage": "early_warning", "dueAt": "2026-10-22T07:00:00.000Z", "met": false },
      "incidentCount": 1
    }
  ],
  "total": 1,
  "page": 1,
  "perPage": 20
}
```

`nextDeadline` ist `{ stage, dueAt, met }` oder `null`.

Statuswerte: `assessment`, `not_reportable`, `reportable`, `early_warning_submitted`, `notification_submitted`, `final_submitted`, `closed`.

## Meldefall öffnen

`POST /api/im/report-cases`

| Feld | Typ | Pflicht | Beschreibung |
|------|-----|---------|--------------|
| `title` | string | Ja | 1 bis 200 Zeichen. |
| `incidentIds` | integer[] | Nein | IM-Vorfälle Ihrer Organisation, höchstens 50, keine Test-Vorfälle. |

`awareAt` beginnt mit dem Zeitpunkt der Anfrage; korrigieren mit `aware-at`. Jeder andere Schlüssel ist ein `400`.

```bash
curl -X POST "$BASE_URL/api/im/report-cases" -H "Cookie: $SESSION_COOKIE" -H "Content-Type: application/json" \
  -d '{ "title": "Ausfall Kundenportal", "incidentIds": [4812] }'
```

`201 Created`: der ganze Fall, gleiche Form wie [Meldefall lesen](#meldefall-lesen).

## Meldefall lesen

`GET /api/im/report-cases/:id`

`200 OK`

```json
{
  "id": 12,
  "publicId": "0b6f9a52-7a43-4c8e-9f1e-5c2d8a1b3e77",
  "organizationId": 2,
  "title": "Ausfall Kundenportal",
  "status": "reportable",
  "awareAt": "2026-10-21T07:00:00.000Z",
  "decision": "reportable",
  "decidedBy": "u_8f2c",
  "decidedAt": "2026-10-21T08:10:00.000Z",
  "decisionReason": null,
  "suspectedMalicious": true,
  "crossBorder": false,
  "severityAssessment": "high",
  "impact": "Kundenportal nicht erreichbar",
  "affectedUsers": null,
  "iocs": "203.0.113.7",
  "rootCause": null,
  "mitigation": null,
  "kritisFacility": null,
  "criticalService": null,
  "interimRequestedAt": null,
  "interimDueAt": null,
  "createdBy": "u_8f2c",
  "createdAt": "2026-10-21T07:05:00.000Z",
  "closedAt": null,
  "canDecide": true,
  "nextDeadline": { "stage": "early_warning", "dueAt": "2026-10-22T07:00:00.000Z", "met": false },
  "deadlines": [
    { "stage": "early_warning", "dueAt": "2026-10-22T07:00:00.000Z", "met": false },
    { "stage": "notification", "dueAt": "2026-10-24T07:00:00.000Z", "met": false }
  ],
  "incidents": [
    { "id": 4812, "title": "Portal weg", "severity": "sev1", "status": "resolved", "triggeredAt": "2026-10-21T06:40:00.000Z", "resolvedAt": "2026-10-21T07:27:00.000Z" }
  ],
  "criteria": [
    { "key": "total_outage_over_30min", "suggestible": true, "suggestedValue": true, "suggestionSource": "monitoring:47min", "decision": "confirmed", "value": true, "decidedBy": "u_8f2c", "decidedAt": "2026-10-21T08:05:00.000Z" }
  ],
  "submissions": [],
  "events": [
    { "id": 301, "type": "decided", "actorUserId": "u_8f2c", "payload": { "decision": "reportable", "reason": null, "previous": null }, "createdAt": "2026-10-21T08:10:00.000Z" }
  ]
}
```

Felder ohne Wert sind `null`, nie weggelassen.

`criteria` enthält immer alle zehn Kriterien (hier gekürzt): `total_outage_over_30min`, `degraded_over_1h_5pct_users`, `financial_loss_500k_or_5pct`, `trade_secret_exfiltration`, `death_or_health_damage`, `malicious_unauthorized_access`, `iva_compromised_malicious`, `iva_compromised_5pct_users`, `third_party_damage`, `planned_maintenance`. Nur das erste und das letzte bekommen einen Vorschlag aus den Monitoring-Daten; ein Vorschlag ist nie eine Entscheidung. `deadlines` bleibt leer, bis der Fall als `reportable` entschieden ist. `events` ist die nur anfügbare Zeitachse, neueste zuerst.

## Inhaltsfelder ändern

`PATCH /api/im/report-cases/:id`

Merge-Patch über die Inhaltsfelder: `suspectedMalicious`, `crossBorder` (boolean oder `null`), `severityAssessment`, `impact`, `affectedUsers`, `iocs`, `rootCause`, `mitigation`, `kritisFacility`, `criticalService` (Text bis 10000 Zeichen oder `null`). Nicht genannte Felder bleiben, `null` leert, leerer Text wird `null`. Jeder andere Schlüssel ist ein `400`. `200 OK`: der ganze Fall.

## Aktionen am Fall

Jede antwortet `200 OK` mit dem ganzen Fall. Jeder andere Schlüssel im Body ist ein `400`.

| Aktion | Body | Regel |
|--------|------|-------|
| `POST /api/im/report-cases/:id/aware-at` | `awareAt` (ISO-Zeitpunkt, Pflicht), `reason` (Text oder `null`) | Früher ist frei; später braucht eine `reason` (`reportCaseAwareAtReasonRequired`). |
| `POST /api/im/report-cases/:id/criteria` | `key` (eines der zehn), `decision` (`confirmed` \| `rejected`), `value` (boolean), alle Pflicht | Fügt eine neue Entscheidung an; die jüngste gilt. |
| `POST /api/im/report-cases/:id/decision` | `decision` (`reportable` \| `not_reportable`, Pflicht), `reason` | Erst wenn jedes Kriterium entschieden ist (`reportCaseCriteriaOpen`). `not_reportable` braucht eine `reason` und geht nach einer Abgabe nicht mehr. |
| `POST /api/im/report-cases/:id/interim-request` | `requestedAt`, `dueAt` (ISO-Zeitpunkte, Pflicht) | Das BSI hat eine Zwischenmeldung angefordert; `dueAt` muss nach `requestedAt` liegen. |
| `POST /api/im/report-cases/:id/close` | keiner | Erst nach der Abschlussmeldung oder bei `not_reportable` (`reportCaseCannotClose`). Ein geschlossener Fall nimmt keine Änderung mehr an. |

```bash
curl -X POST "$BASE_URL/api/im/report-cases/0b6f9a52-7a43-4c8e-9f1e-5c2d8a1b3e77/decision" \
  -H "Cookie: $SESSION_COOKIE" -H "Content-Type: application/json" -d '{ "decision": "reportable" }'
```

## Vorfälle verknüpfen und lösen

`POST /api/im/report-cases/:id/incidents` mit `{ "incidentId": 4813 }` antwortet `201 Created`:

```json
{ "reportCaseId": 12, "reportCasePublicId": "0b6f9a52-7a43-4c8e-9f1e-5c2d8a1b3e77", "incidentId": 4813, "linkedAt": "2026-10-21T09:00:00.000Z" }
```

`DELETE /api/im/report-cases/:id/incidents/:incidentId` antwortet `204 No Content`. Ein zweites DELETE antwortet `404` (`reportCaseIncidentNotLinked`). Verknüpfen und Lösen aktualisieren die Vorschläge aus dem Monitoring.

## Berichtsentwurf

`GET /api/im/report-cases/:id/draft?stage=early_warning&locale=de`

`stage` (Pflicht): `early_warning`, `notification`, `interim`, `progress` oder `final`. `locale`: `de` (Standard) oder `en`. Fehlende Pflichtangaben erscheinen als `[fehlt]` / `[missing]`.

`200 OK`

```json
{
  "text": "# Frühwarnung (≤ 24 h)\n…",
  "draftToken": "v1.1761058800000.de.Qk9…"
}
```

`text` ist Markdown zum Kopieren ins BSI-Portal. `draftToken` ist die Signatur des Servers über genau diesen Text, diesen Fall, diese Stufe und diese Sprache. Behandeln Sie ihn als undurchsichtig, bewahren Sie ihn zusammen mit dem Text auf und schicken Sie beides beim Protokollieren der Abgabe zurück. Er gilt 90 Tage.

## Abgabe protokollieren

`POST /api/im/report-cases/:id/submissions`

Der Ablauf: Entwurf kopieren (`text` und `draftToken`), `text` im BSI-Portal abgeben, danach hier die Abgabe mit demselben `text` und `draftToken` protokollieren. Der Server friert genau diesen Text ein. Weil der Token beweist, dass der Server ihn erzeugt hat, ist der eingefrorene Text zugleich das, was Sie wirklich abgegeben haben, und nichts, was Sie hätten erfinden können. Ein späteres Bearbeiten des Falls ändert den Beleg nicht: ein späterer Entwurf ist ein neuer Text mit neuem Token.

| Feld | Typ | Pflicht | Beschreibung |
|------|-----|---------|--------------|
| `stage` | string | Ja | `early_warning`, `notification`, `interim`, `progress` oder `final`. Muss die Stufe des Entwurfs sein. |
| `submittedAt` | string (ISO-Zeitpunkt) | Ja | Wann Sie im BSI-Portal abgegeben haben; nicht in der Zukunft. |
| `reference` | string | Ja | Aktenzeichen aus dem BSI-Portal, 1 bis 200 Zeichen. |
| `text` | string | Ja | Der Entwurfstext genau so, wie `GET .../draft` ihn geliefert hat, bis 100000 Zeichen. Kein Zeichen darf abweichen. |
| `draftToken` | string | Ja | Der `draftToken`, der mit diesem Text kam. Die Sprache steckt im Token. |

Gehören Text und Token nicht zusammen (anderer Text, Fall oder andere Stufe, oder der Token ist älter als 90 Tage), antwortet der Server `400` (`reportCaseDraftInvalid`): Entwurf neu holen und diesen abgeben. Nur bei Fällen, die als `reportable` entschieden sind. Die Stufen folgen der Reihenfolge des Gesetzes: einmal `early_warning`, dann `notification` (zählt auch als Frühwarnung, wenn keine erfasst ist), danach beliebig viele `interim` und `progress`, zuletzt `final`. Jede andere Reihenfolge antwortet `409` (`reportCaseInvalidTransition`). Jeder andere Schlüssel ist ein `400`.

```bash
curl -X POST "$BASE_URL/api/im/report-cases/0b6f9a52-7a43-4c8e-9f1e-5c2d8a1b3e77/submissions" \
  -H "Cookie: $SESSION_COOKIE" -H "Content-Type: application/json" \
  -d '{ "stage": "early_warning", "submittedAt": "2026-10-21T15:00:00Z", "reference": "BSI-2026-4711", "text": "# Frühwarnung (≤ 24 h)\n…", "draftToken": "v1.1761058800000.de.Qk9…" }'
```

`201 Created`

```json
{ "id": 41, "stage": "early_warning", "submittedAt": "2026-10-21T15:00:00.000Z", "submittedBy": "u_8f2c", "reference": "BSI-2026-4711", "frozenText": "# Frühwarnung (≤ 24 h)\n…", "createdAt": "2026-10-21T15:02:00.000Z" }
```

## Häufige Fehler

- `401 Unauthorized` (`unauthorized`) ohne Anmeldung
- `403 Forbidden` (`imAccessDenied`) für einen Kunden-Login oder eine Rolle ohne Incident-Management-Zugang
- `403 Forbidden` (`nis2NotEnabled`), wenn Meldefälle ausgeschaltet sind
- `403 Forbidden` (`nis2OfficerRequired`) für ein API-Token oder eine Person, die weder Admin noch Meldeverantwortliche ist
- `400 Bad Request` (`invalidIdentifier`), wenn `:id` weder Zahl noch UUID ist
- `404 Not Found` (`reportCaseNotFound`), wenn der Fall nicht existiert oder einer anderen Organisation gehört
- `400 Bad Request` (`invalidRequestBody`) bei fehlendem Feld, falschem Typ, unbekanntem Schlüssel, leerer `reference`, ungültigem Query-Wert oder `dueAt` nicht nach `requestedAt`
- `404 Not Found` (`imIncidentNotFound`), wenn ein Vorfall nicht existiert oder einer anderen Organisation gehört
- `400 Bad Request` (`reportCaseTestIncident`), (`reportCaseReasonRequired`), (`reportCaseAwareAtReasonRequired`), (`reportCaseSubmittedInFuture`), (`reportCaseDraftInvalid`)
- `409 Conflict` (`reportCaseClosed`), (`reportCaseCriteriaOpen`), (`reportCaseAlreadySubmitted`), (`reportCaseNotReportable`), (`reportCaseCannotClose`), (`reportCaseIncidentAlreadyLinked`), (`reportCaseInvalidTransition`)
- `404 Not Found` (`reportCaseIncidentNotLinked`) beim Lösen eines Vorfalls, der nicht verknüpft ist
