Developer docs

Incident API v1

Drive incidents and maintenance windows on your status pages from your own monitoring.

Overview

The Incident API lets your own monitoring (checkMK, Zabbix, Prometheus Alertmanager, a cron script) drive FoundersDeck incidents and scheduled maintenances. It does exactly what the dashboard does: the same validation, the same status page updates and the same subscriber emails.

What the API can do:

  • Open, update and resolve manual incidents on your status pages.
  • Schedule, change and cancel maintenance windows, optionally suppressing alerts for the affected monitors.
  • List your monitors and status pages, so you can reference them by ID.

What the API cannot do (v1):

  • Create, change or delete monitors, status pages, alert channels or subscribers.
  • Read or change automatic incidents that FoundersDeck opens from its own checks.
  • Delete incidents or maintenances (use the dashboard).

Requirements: the API is available on the Pro plan or higher. All requests and responses use JSON (Content-Type: application/json), field names are snake_case, timestamps are ISO 8601 in UTC.

Base URL:

https://foundersdeck.dev/api/v1

Authentication

Every request needs an API key in the Authorization header:

Authorization: Bearer fd_live_<40 characters>
  • Create a key in the dashboard under Settings → API keys. Give it a name (1–60 characters, for example checkMK production).
  • Key format: fd_live_ followed by 40 characters A–Z, a–z, 0–9.
  • Shown once: the full key is displayed only right after creation. FoundersDeck stores only a SHA-256 hash and the first 12 characters (for example fd_live_AbCd) so you can recognise the key in the list. If you lose a key, create a new one.
  • Limit: at most 5 active keys per account.
  • Revoke a key in the same list. Requests with a revoked key fail immediately with 401 key_revoked. Revoking cannot be undone.
  • A key acts on behalf of the account that created it and only sees that account's data. Every change made via the API is recorded in the audit log together with the key that made it.
  • If your account drops below the Pro plan, existing keys stop working (403 plan_required) but stay in the list, so you can still revoke them.

The scheme name Bearer is case-insensitive. Put exactly one space between the scheme and the key.

Rate limits

  • 60 requests per minute per API key. The window starts with the first request and resets after 60 seconds.
  • 30 notification-triggering requests per minute per account, on top of that. These are the calls that can email your status page subscribers: opening an incident (POST /incidents), posting an update (POST /incidents/{ref}/updates), resolving (POST /incidents/{ref}/resolve), scheduling a maintenance (POST /maintenances) and cancelling one (POST /maintenances/{ref}/cancel). This limit counts per account, so several keys share it. Idempotent repeats that return an existing incident or maintenance with 200 (same external_id) do not count.
  • Exceeding either limit returns 429 rate_limited with the header Retry-After: 60. Wait that many seconds before retrying.
  • Only authenticated requests count: requests rejected with 401 or 403 do not use up your quota.

Errors

Every error has the same body:

{
  "error": {
    "code": "validation_failed",
    "message": "Request validation failed",
    "details": [
      {
        "origin": "string",
        "code": "too_small",
        "minimum": 1,
        "inclusive": true,
        "path": ["title"],
        "message": "Too small: expected string to have >=1 characters"
      }
    ]
  }
}

code is stable and meant for your code; message is a human-readable English sentence and may change. details is only present for schema validation errors and lists every failed field with its path.

HTTP code When Example message
401 unauthorized Authorization header missing, not Bearer <key>, key not in fd_live_ format Missing or malformed API key
401 unauthorized Well-formed key that does not exist Invalid API key
401 key_revoked The key was revoked in the dashboard This API key has been revoked
403 plan_required The account is not on Pro or higher This feature requires the Pro plan or higher
404 not_found The {ref} in the path does not exist, belongs to another account or is not a valid UUID / ext: reference Incident not found / Maintenance not found / Not found
404 not_found A monitor_ids / status_page_ids entry or related_incident_id does not belong to your account One or more monitors do not belong to this user
409 conflict A concurrent request created the same external_id first (see Idempotency), or the automatic incident in related_incident_id is already linked to another manual incident Resource already exists
409 conflict An update with a status other than resolved was posted to a resolved incident Incident is already resolved
422 validation_failed Body is not valid JSON Body must be valid JSON
422 validation_failed A field is missing, has the wrong type or is out of range (details lists the fields) Request validation failed
422 validation_failed A change would leave an incident or maintenance without any monitor and status page, or a maintenance window would end before it starts At least one monitor or status page assignment is required
429 rate_limited More than 60 requests per minute with this key Too many requests
429 rate_limited More than 30 notification-triggering requests per minute for this account (see Rate limits) Too many notification-triggering requests
500 internal Unexpected server error; safe to retry with the same external_id Internal error

Checks run in this order: authentication (401), plan (403), request rate limit (429), path reference (404), body (422), notification limit (429, only on the endpoints listed under Rate limits), then the change itself (404, 409, 422). At the path-reference step, a malformed {ref} or an unknown ext: reference returns 404. A well-formed UUID is only looked up with the change itself, so for an unknown UUID an invalid body returns 422 before the 404.

Requests with an HTTP method an endpoint does not support get a plain 405 Method Not Allowed from the web server, not the JSON error body above.

Idempotency with external_id

Monitoring systems retry notifications and sometimes send the same alert twice. Give each problem a stable external_id from your system (1–255 characters, for example checkmk:web01:HTTP) and FoundersDeck will not open duplicates. A longer external_id in a request body is rejected with 422 validation_failed; the checkmk:<host>:<service> ids from the checkMK script normally stay well under that limit.

Incidents (POST /incidents):

  • While an open (not yet resolved) incident with this external_id exists, the request returns that incident unchanged with 200 OK. Nothing is created, no email is sent, and the other fields of the (still validated) body are not applied.
  • If there is no open incident with this external_id (none ever, or all resolved), a new incident is created: 201 Created. A resolved incident therefore frees its external_id for the next outage.
  • Use external_id on every POST your monitoring sends for the same problem, including repeats after WARNING → CRITICAL flaps.

Maintenances (POST /maintenances):

  • While a maintenance with this external_id is active (not cancelled and its scheduled_end is not yet in the past), the request returns it unchanged with 200 OK.
  • Once that window has ended or was cancelled, the same external_id schedules a new maintenance: 201 Created. If the previous window had ended without being cancelled, its external_id is set to null at that moment, so ext: then addresses only the new maintenance; the old one stays reachable by UUID.

Addressing by external_id: every endpoint with {ref} in the path accepts either the FoundersDeck UUID or ext:<external_id>:

POST /api/v1/incidents/ext:checkmk:web01:HTTP/resolve
  • For incidents, ext: points at the open incident with that external_id; if none is open, at the most recently started one. Resolving twice is therefore harmless: the second call returns the already-resolved incident.
  • For maintenances, ext: points only at a maintenance that is not cancelled. A cancelled maintenance is reachable by UUID only.
  • Colons inside the external_id are fine. Percent-encode characters that are not allowed in a URL path, such as spaces (%20) or slashes (%2F): ext:checkmk:db01:Filesystem%20%2Fvar.
  • Unknown external_id in the path, or one longer than 255 characters: 404 not_found.

Concurrent requests: if two POST requests with the same external_id arrive at the same moment, one creates the resource (201) and the other gets 409 conflict. Treat 409 as "already exists" and fetch the resource: for incidents with GET /incidents/ext:<external_id>, for maintenances with GET /maintenances/ext:<external_id>.

external_id is set on creation and cannot be changed afterwards.

Endpoints

Method Path Purpose
GET /monitors List your monitors
GET /status-pages List your status pages
GET /incidents List manual incidents
POST /incidents Open an incident (idempotent with external_id)
GET /incidents/{ref} Get one incident with its timeline
PATCH /incidents/{ref} Change title, severity or assignments
POST /incidents/{ref}/updates Post a timeline update and change the status
POST /incidents/{ref}/resolve Resolve an incident (idempotent)
GET /maintenances List maintenances
POST /maintenances Schedule a maintenance (idempotent with external_id)
GET /maintenances/{ref} Get one maintenance with its assignments
PATCH /maintenances/{ref} Change a maintenance
POST /maintenances/{ref}/cancel Cancel a maintenance (idempotent by UUID; a retry via ext: returns 404)

{ref} is a UUID or ext:<external_id>. Maintenance endpoints need the same plan as incidents (Pro or higher).

Unknown fields in a request body are ignored. Markdown fields (message, description) keep line breaks and are not trimmed. Duplicate entries in monitor_ids / status_page_ids are ignored.

GET /monitors

Lists the monitors of your account, newest first, with the fields you need to reference them.

curl -sS https://foundersdeck.dev/api/v1/monitors \
  -H "Authorization: Bearer $FOUNDERSDECK_API_KEY"

Response 200 OK:

{
  "monitors": [
    {
      "id": "3f6c1d2e-8a4b-4c7d-9e1f-2a3b4c5d6e7f",
      "name": "Website",
      "url": "https://example.org",
      "type": "http"
    }
  ]
}

type is one of http, ping, keyword, heartbeat, dns. url is null for monitor types without a URL (heartbeat).

GET /status-pages

Lists the status pages of your account, newest first.

curl -sS https://foundersdeck.dev/api/v1/status-pages \
  -H "Authorization: Bearer $FOUNDERSDECK_API_KEY"

Response 200 OK:

{
  "status_pages": [
    {
      "id": "9b8a7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "title": "Acme Status",
      "slug": "acme"
    }
  ]
}

The public page is at https://foundersdeck.dev/status/<slug> (or your custom domain).

GET /incidents

Lists your manual incidents, newest first. Optional query parameter status=open returns only incidents that are not resolved.

curl -sS "https://foundersdeck.dev/api/v1/incidents?status=open" \
  -H "Authorization: Bearer $FOUNDERSDECK_API_KEY"

Response 200 OK:

{
  "incidents": [
    {
      "id": "6f1c2a7e-3b9d-4c1e-9a2f-0d4b5e6f7a81",
      "external_id": "checkmk:web01:HTTP",
      "title": "HTTP on web01 unavailable",
      "severity": "major",
      "status": "investigating",
      "started_at": "2026-10-26T09:14:03.512Z",
      "resolved_at": null,
      "url": "https://foundersdeck.dev/dashboard/incidents/manual/6f1c2a7e-3b9d-4c1e-9a2f-0d4b5e6f7a81"
    }
  ]
}

List items do not include assignments or the timeline; fetch a single incident for those. Any other status value returns 422.

POST /incidents

Opens a manual incident, shows it on the assigned status pages and emails their subscribers.

Request body:

Field Type Required Rules
title string yes 1–200 characters after trimming
severity string yes minor, major or critical
status string no investigating (default), identified or monitoring. resolved is not allowed here
message string no Markdown, max. 5000 characters. Becomes the first timeline update
monitor_ids string[] no* Monitor UUIDs of your account. Default []
status_page_ids string[] no* Status page UUIDs of your account. Default []
external_id string no 1–255 characters, see Idempotency
related_incident_id string | null no UUID of one of your automatic incidents to link this one to. An automatic incident can be linked to only one manual incident

* At least one entry in monitor_ids or status_page_ids is required.

curl -sS -X POST https://foundersdeck.dev/api/v1/incidents \
  -H "Authorization: Bearer $FOUNDERSDECK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "HTTP on web01 unavailable",
    "severity": "major",
    "message": "We are investigating failed requests to the web frontend.",
    "status_page_ids": ["9b8a7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d"],
    "external_id": "checkmk:web01:HTTP"
  }'

Response 201 Created (new incident) or 200 OK (an open incident with this external_id already existed and is returned unchanged):

{
  "id": "6f1c2a7e-3b9d-4c1e-9a2f-0d4b5e6f7a81",
  "external_id": "checkmk:web01:HTTP",
  "title": "HTTP on web01 unavailable",
  "severity": "major",
  "status": "investigating",
  "started_at": "2026-10-26T09:14:03.512Z",
  "resolved_at": null,
  "monitor_ids": [],
  "status_page_ids": ["9b8a7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d"],
  "updates": [
    {
      "id": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
      "status": "investigating",
      "message": "We are investigating failed requests to the web frontend.",
      "created_at": "2026-10-26T09:14:03.512Z"
    }
  ],
  "url": "https://foundersdeck.dev/dashboard/incidents/manual/6f1c2a7e-3b9d-4c1e-9a2f-0d4b5e6f7a81"
}

This is the full incident resource, returned by every single-incident endpoint. updates is the timeline in chronological order (oldest first); it is empty when the incident was opened without message. url links to the incident in the dashboard.

GET /incidents/{ref}

Returns one incident with its assignments and timeline.

curl -sS https://foundersdeck.dev/api/v1/incidents/ext:checkmk:web01:HTTP \
  -H "Authorization: Bearer $FOUNDERSDECK_API_KEY"

Response 200 OK: the full incident resource (see POST /incidents).

PATCH /incidents/{ref}

Changes title, severity or assignments. Send only the fields you want to change; at least one is required. This does not post a timeline update and sends no emails. To change the status, use /updates or /resolve.

Field Type Rules
title string 1–200 characters after trimming
severity string minor, major or critical
monitor_ids string[] Replaces all monitor assignments
status_page_ids string[] Replaces all status page assignments

After the change, the incident must still have at least one monitor or status page.

curl -sS -X PATCH https://foundersdeck.dev/api/v1/incidents/ext:checkmk:web01:HTTP \
  -H "Authorization: Bearer $FOUNDERSDECK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"severity": "critical"}'

Response 200 OK: the full incident resource.

POST /incidents/{ref}/updates

Adds an update to the timeline, sets the incident's status and emails the subscribers of the assigned status pages.

Field Type Required Rules
status string yes investigating, identified, monitoring or resolved
message string yes Markdown, 1–5000 characters, not only whitespace

status: "resolved" resolves the incident (sets resolved_at). If the incident is already resolved, a resolved update is ignored: no new update, no email, the incident is returned unchanged. Any other status on a resolved incident returns 409 conflict: a resolved incident is not reopened. For a new outage after a resolve, open a new incident.

An update with the same status and message as the incident's latest update is a no-op: 200 OK with the incident unchanged, no new update and no email. Retrying a request is therefore safe.

curl -sS -X POST https://foundersdeck.dev/api/v1/incidents/ext:checkmk:web01:HTTP/updates \
  -H "Authorization: Bearer $FOUNDERSDECK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "identified", "message": "A failed deployment caused the errors. Rolling back."}'

Response 200 OK: the full incident resource including the new update.

POST /incidents/{ref}/resolve

Resolves the incident. Shortcut for an update with status: "resolved".

Field Type Required Rules
message string no Markdown, max. 5000 characters. Default Resolved.

The body may be empty. Resolving an already-resolved incident changes nothing and returns 200 with the incident, so retries are safe.

curl -sS -X POST https://foundersdeck.dev/api/v1/incidents/ext:checkmk:web01:HTTP/resolve \
  -H "Authorization: Bearer $FOUNDERSDECK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message": "Service recovered."}'

Response 200 OK: the full incident resource with status: "resolved" and resolved_at set.

GET /maintenances

Lists your maintenances, latest scheduled_start first, including past and cancelled ones.

curl -sS https://foundersdeck.dev/api/v1/maintenances \
  -H "Authorization: Bearer $FOUNDERSDECK_API_KEY"

Response 200 OK:

{
  "maintenances": [
    {
      "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
      "external_id": "change-4711",
      "title": "Database upgrade",
      "description": "PostgreSQL minor upgrade. Expect up to 5 minutes of downtime.",
      "scheduled_start": "2026-11-02T21:00:00.000Z",
      "scheduled_end": "2026-11-02T22:00:00.000Z",
      "suppress_alerts": true,
      "cancelled_at": null,
      "cancellation_message": null,
      "url": "https://foundersdeck.dev/dashboard/maintenance/1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
    }
  ]
}

List items do not include assignments; GET /maintenances/{ref}, PATCH and cancel return the full resource with monitor_ids and status_page_ids.

POST /maintenances

Schedules a maintenance window, shows it on the assigned status pages and emails their subscribers.

Field Type Required Rules
title string yes 1–200 characters after trimming
description string yes Markdown, 1–5000 characters, not only whitespace
scheduled_start string yes ISO 8601 date-time with seconds and a time zone, e.g. 2026-11-02T22:00:00+01:00 or 2026-11-02T21:00:00Z
scheduled_end string yes Same format, must be after scheduled_start
suppress_alerts boolean no Default true: no alerts for the assigned monitors during the window
monitor_ids string[] no* Monitor UUIDs of your account. Default []
status_page_ids string[] no* Status page UUIDs of your account. Default []
external_id string no 1–255 characters, see Idempotency

* At least one entry in monitor_ids or status_page_ids is required.

curl -sS -X POST https://foundersdeck.dev/api/v1/maintenances \
  -H "Authorization: Bearer $FOUNDERSDECK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Database upgrade",
    "description": "PostgreSQL minor upgrade. Expect up to 5 minutes of downtime.",
    "scheduled_start": "2026-11-02T22:00:00+01:00",
    "scheduled_end": "2026-11-02T23:00:00+01:00",
    "monitor_ids": ["3f6c1d2e-8a4b-4c7d-9e1f-2a3b4c5d6e7f"],
    "status_page_ids": ["9b8a7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d"],
    "external_id": "change-4711"
  }'

Response 201 Created (new maintenance) or 200 OK (an active maintenance with this external_id already existed and is returned unchanged):

{
  "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
  "external_id": "change-4711",
  "title": "Database upgrade",
  "description": "PostgreSQL minor upgrade. Expect up to 5 minutes of downtime.",
  "scheduled_start": "2026-11-02T21:00:00.000Z",
  "scheduled_end": "2026-11-02T22:00:00.000Z",
  "suppress_alerts": true,
  "cancelled_at": null,
  "cancellation_message": null,
  "monitor_ids": ["3f6c1d2e-8a4b-4c7d-9e1f-2a3b4c5d6e7f"],
  "status_page_ids": ["9b8a7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d"],
  "url": "https://foundersdeck.dev/dashboard/maintenance/1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}

Times are always returned in UTC.

GET /maintenances/{ref}

Returns one maintenance with its assignments. As everywhere, ext: only finds a maintenance that is not cancelled.

curl -sS https://foundersdeck.dev/api/v1/maintenances/ext:change-4711 \
  -H "Authorization: Bearer $FOUNDERSDECK_API_KEY"

Response 200 OK: the full maintenance resource (see POST /maintenances).

PATCH /maintenances/{ref}

Changes a maintenance. Send only the fields you want to change; at least one is required. Sends no emails.

Field Type Rules
title string 1–200 characters after trimming
description string Markdown, 1–5000 characters, not only whitespace
scheduled_start string ISO 8601 date-time with seconds and a time zone
scheduled_end string ISO 8601 date-time with seconds and a time zone
suppress_alerts boolean
monitor_ids string[] Replaces all monitor assignments
status_page_ids string[] Replaces all status page assignments

The resulting window must end after it starts (checked against the stored values if you send only one end), and the maintenance must keep at least one monitor or status page. external_id cannot be changed.

curl -sS -X PATCH https://foundersdeck.dev/api/v1/maintenances/ext:change-4711 \
  -H "Authorization: Bearer $FOUNDERSDECK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"scheduled_end": "2026-11-02T23:30:00+01:00"}'

Response 200 OK: the full maintenance resource (see POST /maintenances).

POST /maintenances/{ref}/cancel

Cancels a maintenance and emails the subscribers of the assigned status pages.

Field Type Required Rules
message string no Markdown, max. 5000 characters. Stored as cancellation_message

The body may be empty. Cancelling an already-cancelled maintenance by UUID changes nothing and returns 200 with it. Retrying a cancel via ext:<external_id> returns 404 not_found, because a cancelled maintenance is only addressable by UUID. For safe retries, take the id from the first response and retry with that UUID.

curl -sS -X POST https://foundersdeck.dev/api/v1/maintenances/ext:change-4711/cancel \
  -H "Authorization: Bearer $FOUNDERSDECK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message": "Postponed to next week."}'

Response 200 OK: the full maintenance resource with cancelled_at set.

Example: checkMK notification script

This script turns checkMK notifications into FoundersDeck incidents: CRITICAL/DOWN opens an incident on your status page, OK/UP resolves it. The external_id checkmk:<host>:<service> makes repeated notifications for the same problem harmless. WARNING, UNKNOWN and other states are ignored.

Requirements on the checkMK server: bash, curl (7.76 or newer, for --fail-with-body) and jq. A failed API call exits non-zero and its error body shows up in checkMK's notification log. On OK/UP, a 404 counts as success: there is no incident to resolve, for example after a recovery from WARNING, which never opened one. Title and message are cut to the API limits (200 and 5000 characters) so a long plugin output cannot fail the call.

#!/usr/bin/env bash
# FoundersDeck notification script for checkMK (place in ~/local/share/check_mk/notifications/)
set -euo pipefail
API="https://foundersdeck.dev/api/v1"
KEY="${FOUNDERSDECK_API_KEY:?set FOUNDERSDECK_API_KEY}"
STATUS_PAGE_ID="${FOUNDERSDECK_STATUS_PAGE_ID:?set FOUNDERSDECK_STATUS_PAGE_ID}"

HOST="${NOTIFY_HOSTNAME}"
SERVICE="${NOTIFY_SERVICEDESC:-host}"
STATE="${NOTIFY_SERVICESTATE:-$NOTIFY_HOSTSTATE}"
EXT_ID="checkmk:${HOST}:${SERVICE}"
AUTH=(-H "Authorization: Bearer ${KEY}" -H "Content-Type: application/json")

case "$STATE" in
  CRITICAL|DOWN)
    curl -sS --fail-with-body -X POST "${API}/incidents" "${AUTH[@]}" -d "$(jq -n \
      --arg t "${SERVICE} on ${HOST} unavailable" --arg e "$EXT_ID" --arg sp "$STATUS_PAGE_ID" \
      --arg m "${NOTIFY_SERVICEOUTPUT:-$NOTIFY_HOSTOUTPUT}" \
      '{title:($t|.[0:200]), severity:"major", message:($m|.[0:5000]), status_page_ids:[$sp], external_id:$e}')" ;;
  OK|UP)
    # Service names may contain spaces or slashes (e.g. "Filesystem /var"): percent-encode the id for the URL path.
    EXT_ID_ENC=$(jq -rn --arg s "$EXT_ID" '$s|@uri')
    RESP=$(curl -sS -w '\n%{http_code}' -X POST "${API}/incidents/ext:${EXT_ID_ENC}/resolve" "${AUTH[@]}" \
      -d '{"message":"Service recovered."}')
    CODE="${RESP##*$'\n'}"
    # 404 = no incident with this id (e.g. recovery after a WARNING that never opened one): nothing to resolve.
    case "$CODE" in
      200|404) exit 0 ;;
      *) printf '%s\n' "${RESP%$'\n'*}" >&2; exit 1 ;;
    esac ;;
  *) exit 0 ;;
esac

Setup:

  1. Create an API key under Settings → API keys and look up your status page ID with GET /status-pages.
  2. Save the script as ~/local/share/check_mk/notifications/foundersdeck in your checkMK site and make it executable (chmod +x).
  3. Make FOUNDERSDECK_API_KEY and FOUNDERSDECK_STATUS_PAGE_ID available to the site's notification process, for example in the site's ~/etc/environment.
  4. Create a notification rule in checkMK that uses the script for the hosts and services that should appear on your status page.

The resolve URL percent-encodes the external_id (jq ... '@uri') because it goes into the URL path; the POST /incidents body sends it as plain JSON.

Changelog

  • v1 — 2026-10: initial release.