---
title: "Postmortems and Follow-up"
description: "Record the root cause, lessons learned and actions after an incident, keep a catalog of causes, and track follow-up."
---

`GET /api/im/causes` · `POST /api/im/causes` · `PATCH /api/im/causes/:id`

`GET /api/im/incidents/:id/postmortem` · `PUT /api/im/incidents/:id/postmortem` · `POST /api/im/incidents/:id/postmortem/complete` · `POST /api/im/incidents/:id/postmortem/reopen`

`GET /api/im/postmortems` · `GET /api/im/postmortems/:id` · `POST /api/im/postmortems/:id/actions` · `PATCH /api/im/postmortems/:id/actions/:actionId` · `DELETE /api/im/postmortems/:id/actions/:actionId`

`GET /api/im/follow-up` · `GET /api/im/quarterly-reviews` · `POST /api/im/quarterly-reviews`

After an incident is resolved you can write one postmortem for it: summary, impact, root cause, a cause from your organization's catalog, lessons learned and actions. The catalog cause is what makes repeated incidents countable: with NIS-2 switched on, causes that appear in at least two completed postmortems within six months show up as repeated (Art. 4 Implementing Regulation (EU) 2024/2690). **Uptimeify only counts. Whether a repetition is significant is your decision, through a [report case](/api/incident-management/report-cases).**

A postmortem keeps a copy of the incident data. When the incident is deleted after 12 months, `incidentId` becomes `null` and the postmortem stays for three years after completion.

`:id` under `/incidents` is the numeric ID of an IM incident. `:id` under `/postmortems` and `/causes` and `:actionId` are a `publicId` or a numeric `id`.

## Authentication

Postmortems and actions work with a session, an organization-wide API token or an agent token, like the other incident endpoints. Reading needs access to the incident; writing needs membership of the incident's team or the `admin` role (`403 imTeamMembershipRequired`). Creating or changing causes and reopening a completed postmortem need a session of an organization admin or a [reporting officer](/api/incident-management/nis2-settings) (`403 nis2OfficerRequired`). Postmortems of deleted incidents are only visible to them as well. An incident, postmortem or cause of another organization answers `404`, the same as a missing one.

## Causes

The catalog is shared by every postmortem of your organization. Reading works with a session or a token; creating and changing causes needs a **signed-in session** of an organization admin or a reporting officer. API and agent tokens get `403 nis2OfficerRequired` there, so the requests below run from the dashboard session and are shown without a token.

`GET /api/im/causes` answers `{ items, total, page, perPage }`, sorted by name.

| Query | Description |
|-------|-------------|
| `includeArchived` | `true` or `1` also lists archived causes. Default `false`. |
| `page` | Default `1`. |
| `perPage` | Default `50`, at most `200`. |

`POST /api/im/causes` creates a cause and answers `201` with it.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | 1 to 200 characters, unique per organization regardless of case (`409 causeNameTaken`). |
| `description` | string or null | No | At most 2000 characters. |

`PATCH /api/im/causes/:id` changes `name`, `description` or `archived` (`true` archives, `false` restores) and answers the cause. A cause of another organization or an unknown one answers `404 causeNotFound`, a name already in use `409 causeNameTaken`. Causes are never deleted: archived causes stay on old postmortems and still count for repeated incidents.

```http
POST /api/im/causes
Content-Type: application/json

{ "name": "Expired TLS certificate" }
```

`201 Created`

```json
{
  "id": 3,
  "publicId": "0b7f1c52-6a0e-4c4f-9d1e-2f7c9a3b8e11",
  "name": "Expired TLS certificate",
  "description": null,
  "archivedAt": null,
  "createdAt": "2026-10-22T09:00:00.000Z",
  "updatedAt": "2026-10-22T09:00:00.000Z"
}
```

| Field | Description |
|-------|-------------|
| `archivedAt` | `null` while active. Archived causes are hidden from the picker, stay on old postmortems and still count for repeated incidents. |

## Postmortem of an incident

`GET /api/im/incidents/:id/postmortem` answers `404 postmortemNotFound` until one exists. An incident of another organization or an unknown one answers `404 imIncidentNotFound`.

`PUT /api/im/incidents/:id/postmortem` creates the postmortem on the first call and afterwards merges the fields you send (`null` clears, empty text becomes `null`). Unknown fields or wrong types answer `400 invalidRequestBody`. The incident must be resolved (`409 postmortemIncidentNotResolved`). A completed postmortem accepts no content changes (`409 postmortemCompleted`). `causeId` must be a cause of your organization (`404 causeNotFound`); an archived cause can stay on the postmortem but cannot be newly chosen (`409 causeArchived`).

| Field | Type | Description |
|-------|------|-------------|
| `summary` | string or null | At most 10000 characters. Required to complete. |
| `impact` | string or null | At most 10000 characters. |
| `rootCause` | string or null | Root cause in your words, at most 10000 characters. Required to complete. |
| `causeId` | number or null | A cause from the catalog, not archived. Required to complete. |
| `lessonsLearned` | string or null | At most 10000 characters. May stay empty. |

`POST /api/im/incidents/:id/postmortem/complete` completes it and refreshes the copy of the incident data. It answers `400 postmortemIncomplete` without `summary` or `rootCause`, `400 postmortemCauseRequired` without `causeId`, `409 postmortemCompleted` when it is already completed and `409 postmortemIncidentNotResolved` when the incident is no longer resolved.

`POST /api/im/incidents/:id/postmortem/reopen` turns it back into a draft. It needs a session of an organization admin or a reporting officer (`403 nis2OfficerRequired`), answers `404 postmortemNotFound` when there is no postmortem yet, and a postmortem whose incident was deleted cannot be reopened (`409 postmortemOrphaned`). Reopening a draft changes nothing. Both endpoints answer the whole postmortem.

```bash
curl -X PUT "$BASE_URL/api/im/incidents/42/postmortem" \
  -H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" \
  -d '{ "summary": "Status page unreachable for 47 minutes", "rootCause": "Certificate expired", "causeId": 3 }'
```

`200 OK`

```json
{
  "id": 5,
  "publicId": "6f0c1c1e-3c55-4b55-9d7e-1f2a3b4c5d6e",
  "organizationId": 7,
  "incidentId": 42,
  "originalIncidentId": 42,
  "incidentTitle": "Status page unreachable",
  "incidentSeverity": "sev2",
  "incidentTriggeredAt": "2026-10-20T08:00:00.000Z",
  "incidentResolvedAt": "2026-10-20T08:47:00.000Z",
  "summary": "Status page unreachable for 47 minutes",
  "impact": null,
  "rootCause": "Certificate expired",
  "causeId": 3,
  "causeName": "Expired TLS certificate",
  "lessonsLearned": null,
  "status": "draft",
  "completedBy": null,
  "completedAt": null,
  "createdBy": null,
  "createdAt": "2026-10-22T09:05:00.000Z",
  "updatedAt": "2026-10-22T09:05:00.000Z",
  "actions": [
    {
      "id": 11,
      "publicId": "a3c1e9d2-7b4f-4e0a-8c5d-1f2e3a4b5c6d",
      "title": "Monitor certificate expiry",
      "ownerUserId": "u_8f2c",
      "ownerName": "Jana Weber",
      "dueOn": "2026-11-30",
      "status": "open",
      "doneAt": null,
      "isOverdue": false,
      "createdAt": "2026-10-22T09:06:00.000Z"
    }
  ]
}
```

| Field | Description |
|-------|-------------|
| `incidentId` | `null` once the incident was deleted (after 12 months). |
| `originalIncidentId` | The incident this postmortem belongs to. Stays after the incident is deleted. |
| `incidentTitle`, `incidentSeverity`, `incidentTriggeredAt`, `incidentResolvedAt` | Copy of the incident data, refreshed on completion. |
| `status` | `draft` or `completed`. |
| `createdBy`, `completedBy` | User ID, `null` when written with an API or agent token. |
| `actions[].isOverdue` | `true` when the action is `open` and `dueOn` is before today in your organization's time zone. |

## Postmortems by ID

`GET /api/im/postmortems` lists postmortems, newest incident first, in the form `{ items, total, page, perPage }`. Postmortems of deleted incidents are only listed for organization admins and reporting officers.

| Query | Description |
|-------|-------------|
| `status` | `draft` or `completed`. |
| `page` | Default `1`. |
| `perPage` | Default `50`, at most `200`. |

Each entry has `id`, `publicId`, `incidentId`, `originalIncidentId`, `incidentTitle`, `incidentSeverity`, `incidentTriggeredAt`, `status`, `causeId`, `causeName`, `completedAt` and `updatedAt`.

`GET /api/im/postmortems/:id` returns one postmortem in the same shape as above, also after its incident was deleted (then only for organization admins and reporting officers). Unknown, foreign or not visible: `404 postmortemNotFound`.

## Actions

Actions can be added and changed also after the postmortem is completed. Writing needs the same rights as writing the postmortem.

`POST /api/im/postmortems/:id/actions` answers `201` with the action.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | string | Yes | 1 to 500 characters. |
| `ownerUserId` | string or null | No | A user of your organization (`400 postmortemActionOwnerInvalid`). |
| `dueOn` | string or null | No | Calendar date `YYYY-MM-DD`, not a timestamp (`400 invalidRequestBody`). Open actions after this day are `isOverdue`. |

`PATCH /api/im/postmortems/:id/actions/:actionId` changes `title`, `ownerUserId`, `dueOn` or `status` (`open`, `done`, `dropped`) and answers the action. `done` sets `doneAt`. `DELETE /api/im/postmortems/:id/actions/:actionId` answers `204` without a body; a second `DELETE` answers `404 postmortemActionNotFound`, as does an action of another postmortem.

```bash
curl -X POST "$BASE_URL/api/im/postmortems/5/actions" \
  -H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" \
  -d '{ "title": "Monitor certificate expiry", "dueOn": "2026-11-30" }'
```

`201 Created` with the action in the shape of `actions[]` above.

## Follow-up (NIS-2)

`GET /api/im/follow-up` needs NIS-2 switched on (`403 nis2NotEnabled`) and a **signed-in session** of an organization admin or reporting officer. API and agent tokens and other users get `403 nis2OfficerRequired`, so the requests in this section run from the dashboard session and are shown without a token. It returns four things at once:

| Field | Description |
|-------|-------------|
| `notFollowedUp` | Resolved incidents that need a postmortem (severity at or above `postmortemMinSeverity`, or a linked report case) and have no completed one. Test and merged incidents never count. `postmortemStatus` is `none` or `draft`. |
| `openActions` | Open actions with `isOverdue`, earliest due first. |
| `repeatedCauses` | Causes with at least two completed postmortems whose incidents started within the last six calendar months, with the incidents (also deleted ones, from the postmortem's copy; then `incidentId` is `null`). Merged incidents do not count. |
| `dueQuarter` | The previous quarter while it is not signed off, otherwise `null`. |

Quarters and due dates use the time zone from the [NIS-2 settings](/api/incident-management/nis2-settings) (default `Europe/Berlin`), returned as `timezone` together with `postmortemMinSeverity` and the time of calculation `asOf`.

```http
GET /api/im/follow-up
```

`200 OK`

```json
{
  "asOf": "2026-10-22T09:00:00.000Z",
  "timezone": "Europe/Berlin",
  "postmortemMinSeverity": "sev2",
  "dueQuarter": "2026-Q3",
  "notFollowedUp": [
    { "incidentId": 812, "title": "Checkout returns 502", "severity": "sev1", "triggeredAt": "2026-10-20T07:12:00.000Z", "resolvedAt": "2026-10-20T08:01:00.000Z", "hasReportCase": true, "postmortemStatus": "draft" }
  ],
  "openActions": [
    { "id": 17, "publicId": "5d1e2f3a-4b5c-4d6e-8f70-81a2b3c4d5e6", "title": "Monitor certificate expiry", "ownerUserId": "u_8f2c", "ownerName": "Jana Weber", "dueOn": "2026-10-15", "isOverdue": true, "postmortemPublicId": "6f0c1c1e-3c55-4b55-9d7e-1f2a3b4c5d6e", "incidentId": 790, "incidentTitle": "TLS handshake fails" }
  ],
  "repeatedCauses": [
    {
      "causeId": 3,
      "causePublicId": "0b7f1c52-6a0e-4c4f-9d1e-2f7c9a3b8e11",
      "causeName": "Expired TLS certificate",
      "causeArchivedAt": null,
      "incidentCount": 2,
      "incidents": [
        { "postmortemPublicId": "6f0c1c1e-3c55-4b55-9d7e-1f2a3b4c5d6e", "incidentId": 790, "originalIncidentId": 790, "incidentTitle": "TLS handshake fails", "incidentTriggeredAt": "2026-09-14T22:40:00.000Z" },
        { "postmortemPublicId": "9a8b7c6d-1e2f-4a3b-8c4d-5e6f7a8b9c0d", "incidentId": null, "originalIncidentId": 655, "incidentTitle": "API certificate expired", "incidentTriggeredAt": "2026-05-02T06:05:00.000Z" }
      ]
    }
  ]
}
```

## Quarterly review (NIS-2)

Same access as the follow-up: NIS-2 switched on (`403 nis2NotEnabled`), session of an organization admin or reporting officer (`403 nis2OfficerRequired`).

`POST /api/im/quarterly-reviews` with `{ "quarter": "2026-Q3", "note": "Checked with the ISMS team" }` signs off a quarter that has ended (`409 quarterlyReviewNotDue` before) and answers `201`. The earliest quarter is `2023-Q1` (NIS-2 Directive in force since 16 January 2023); an earlier one answers `400 invalidRequestBody`. Each quarter can be signed off once (`409 quarterlyReviewExists`), also later. The response carries a `snapshot` frozen at sign-off: repeated causes as of the end of the quarter, open and overdue actions (count and `publicId`s) and incidents not followed up at the time of signing. Sign-offs cannot be changed or deleted and are kept for three years. At the start of each quarter the reporting officers (or, without officers, the organization admins) get one email that the review is due.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `quarter` | string | Yes | Calendar quarter in your organization's time zone, `YYYY-QN` such as `2026-Q3`, `2023-Q1` at the earliest. |
| `note` | string or null | No | At most 5000 characters. Empty text becomes `null`. |

Another format, a quarter before `2023-Q1`, a note that is too long or an unknown field answers `400 invalidRequestBody`.

```http
POST /api/im/quarterly-reviews
Content-Type: application/json

{ "quarter": "2026-Q3", "note": "Checked with the ISMS team" }
```

`201 Created`

```json
{
  "id": 1,
  "publicId": "2c9e4f1a-8b3d-4e6f-a1c2-3d4e5f6a7b8c",
  "quarter": "2026-Q3",
  "reviewedBy": "u_8f2c",
  "reviewerName": "Jana Weber",
  "reviewedAt": "2026-10-02T08:30:00.000Z",
  "note": "Checked with the ISMS team",
  "snapshot": {
    "asOf": "2026-09-30T21:59:59.999Z",
    "repeatedCauses": [{ "causeId": 3, "causeName": "Expired TLS certificate", "incidentCount": 2, "postmortemPublicIds": ["6f0c1c1e-3c55-4b55-9d7e-1f2a3b4c5d6e", "9a8b7c6d-1e2f-4a3b-8c4d-5e6f7a8b9c0d"] }],
    "openActionCount": 1,
    "openActionPublicIds": ["5d1e2f3a-4b5c-4d6e-8f70-81a2b3c4d5e6"],
    "overdueActionCount": 0,
    "overdueActionPublicIds": [],
    "notFollowedUpIncidentIds": []
  }
}
```

`GET /api/im/quarterly-reviews?page=1&perPage=50` lists the sign-offs, newest quarter first, as `{ items, total, page, perPage }` (`perPage` at most 200). Each item has the shape above.

## Common errors

See [Postmortem and follow-up error codes](/api/error-codes-and-known-pitfalls#postmortem-and-follow-up-error-codes).
