api reference
The 137 operations of the hub's REST API, the same the web UI uses. Every hub serves this document at /api/openapi.json and an interactive Swagger UI at /api/docs, where you can try requests with your session or a token. Download it here as openapi.json (OpenAPI 3.1, latest version on main) to generate a client.
──authentication
Create a token in settings → api tokens and send it as a bearer token. Bodies are JSON, times are Unix milliseconds, errors are {"error": "..."}.
curl -H "Authorization: Bearer ptk_..." https://status.example.com/api/monitorscurl -X POST -H "Authorization: Bearer ptk_..." -H "Content-Type: application/json" \-d '{"name":"Website","type":"http","target":"https://example.com"}' \https://status.example.com/api/monitors
Each endpoint is marked with who can call it:
- public: no authentication
- read token: any user or API token, a read token is enough
- write token · admin: an admin, with a write token or the browser session; viewers and read tokens get 403
- browser session: browser session only, API tokens get 403
- admin session: browser session of an admin only
Viewers (read-only users) see passwords in monitor targets, headers and notification channels as ********.
──monitors
Checks, their history and heartbeat (push) URLs.
/api/monitorsList monitorsread tokenEvery monitor with its last 24 hours. Secrets (passwords in targets, headers, bodies, push tokens) are hidden from read-only users.
| group | query · string | Group id, or none for monitors without a group. |
| tag | query · integer | Tag id. |
response 200 · Monitors.
[{"name": null,"type": null,"target": null,"interval_sec": null,"timeout_ms": null,"retries": null,"method": null,"expected_status": null,"keyword": null,"keyword_invert": null,"dns_record": null,"dns_server": null,"dns_expected": null,"ignore_tls": null,"ip_family": null,"headers": null,"body": null,"assertions": null,"grace_sec": null,"degraded_pct": null,"locations": null,"group_id": null,"tag_ids": null,"notifiers": null,"cert_expiry_alert": null,"domain_expiry_alert": null,"paused": null,"id": 1,"push_token": "string","status": "up","status_since": 1761868800000,"created_at": 1761868800000,"uptime_24h": 12.5,"avg_ms_24h": 12.5,"last": {"id": null,"monitor_id": null,"location": null,"ts": null,"ok": null,"latency_ms": null,"status_code": null,"message": null,"cert_expiry": null,"maint": null,"timing": null},"bars": [{"t": null,"total": null,"ok": null,"degraded": null,"maint": null,"avg_ms": null}],"expiry": [{"monitor_id": null,"kind": null,"subject": null,"expires_at": null,"issuer": null,"source": null,"error": null,"checked_at": null}],"maintenance": {"id": 1,"name": "string","end_at": 1761868800000}}]
/api/monitorsCreate a monitorwrite token · admin| name* | string | |
| type* | string | one of: http keyword tcp ping dns push postgres mysql redis mongodb docker grpc smtp mcp |
| target | string | What to check, depends on the type: an http(s) URL (http, keyword, mcp), host:port (tcp, IPv6 in brackets: [2001:db8::1]:443), a hostname or IP (ping, dns), a connection URL (postgres://, mysql://, redis://, mongodb://), a container name or id (docker), grpc[s]://host:port[/service], smtp[s]://host[:port]. Empty for push. |
| interval_sec | integer | Seconds between checks, 10 to 86400 (push: expected period, up to 7 days). 0 = default (60, push 3600). |
| timeout_ms | integer | 500 to 60000. 0 = 10000. |
| retries | integer | Consecutive failures before a location counts as down, 1 to 10. |
| method | string | HTTP method for http/keyword (default GET). For mcp: the transport, auto, streamable or sse. |
| expected_status | string | Accepted HTTP status codes, e.g. "200-299" or "200,301". For mcp: the minimum number of tools. |
| keyword | string | keyword: text that must appear in the body. mcp: comma separated tool names that must be listed. |
| keyword_invert | boolean | keyword: fail when the text is found. |
| dns_record | string | dns: record type (A, AAAA, CNAME, MX, TXT, NS...). |
| dns_server | string | dns: resolver to query, host[:port]. Empty = system resolver. |
| dns_expected | string | dns: value that must be in the answer. |
| ignore_tls | boolean | Accept invalid TLS certificates. |
| ip_family | integer | 0 = instance default (GeneralSettings.ip_family), 1 = IPv4 or IPv6 (whichever connects first), 4 = IPv4 only, 6 = IPv6 only, 46 = IPv4 and IPv6 (both checked, the check fails when either fails). Locations without IPv6 check over IPv4 with 0 and 1; 6 and 46 are refused on them (see Location.ipv6). Ignored for push and docker.one of: 0 1 4 6 46 |
| headers | object | Request headers (http, keyword, mcp). |
| body | string | Request body (http, keyword). |
| assertions | Assertion[] | JSON response rules, http and keyword only, at most 20. |
| grace_sec | integer | push: extra time after the period before the monitor goes down, 0 to 86400. |
| degraded_pct | integer | Share of failing locations (percent) from which the monitor is degraded, 1 to 50. 0 = instance default. |
| locations | string[] | Where the check runs: "local" (the hub) and/or "agent:<id>". Empty = local. See GET /api/locations. |
| group_id | integer | Group, 0 = none. |
| tag_ids | integer[] | |
| notifiers | ChannelLink[] | Notification channels linked to this monitor one by one (notifier_id + events). Channels whose scope covers the monitor need no link. Left out on update: the links are kept; [] removes them. |
| cert_expiry_alert | boolean | Alert before the TLS certificate expires (https targets). |
| domain_expiry_alert | boolean | Alert before the domain registration expires. |
| paused | boolean | Create the monitor paused. Ignored on update: use POST /api/monitors/{id}/pause. |
{"name": "Website","type": "http","target": "https://example.com","interval_sec": 60,"timeout_ms": 10000,"retries": 2,"method": "GET","expected_status": "200-299","locations": ["local"],"tag_ids": [],"group_id": 0,"assertions": [{"path": "status","op": "eq","value": "ok"}],"cert_expiry_alert": true}
response 201 · Created.
{"name": "Website","type": "http","target": "https://example.com","interval_sec": 60,"timeout_ms": 10000,"retries": 2,"method": "GET","expected_status": "200-299","locations": ["local"],"tag_ids": [],"group_id": 0,"assertions": [{"path": "status","op": "eq","value": "ok"}],"cert_expiry_alert": true,"id": 7,"headers": {},"body": "","keyword": "","keyword_invert": false,"dns_record": "","dns_server": "","dns_expected": "","ignore_tls": false,"push_token": "","grace_sec": 0,"degraded_pct": 0,"ip_family": 0,"domain_expiry_alert": false,"paused": false,"status": "up","status_since": 1761868800000,"created_at": 1761868800000}
/api/monitors/{id}Get a monitorread tokenIncludes 7 and 30 day uptime and the last result of each location.
| id* | path · integer | Id of the monitor. |
response 200 · Monitor.
{"id": null,"push_token": null,"status": null,"status_since": null,"created_at": null,"uptime_24h": 12.5,"avg_ms_24h": 12.5,"last": null,"bars": [null],"expiry": [null],"maintenance": {"id": null,"name": null,"end_at": null},"uptime_7d": 12.5,"uptime_30d": 12.5,"location_status": [{"id": "string","name": "string","online": true,"last": null}],"push_url": "string"}
/api/monitors/{id}Update a monitorwrite token · adminReplaces the monitor: send every field.
| id* | path · integer | Id of the monitor. |
| name* | string | |
| type* | string | one of: http keyword tcp ping dns push postgres mysql redis mongodb docker grpc smtp mcp |
| target | string | What to check, depends on the type: an http(s) URL (http, keyword, mcp), host:port (tcp, IPv6 in brackets: [2001:db8::1]:443), a hostname or IP (ping, dns), a connection URL (postgres://, mysql://, redis://, mongodb://), a container name or id (docker), grpc[s]://host:port[/service], smtp[s]://host[:port]. Empty for push. |
| interval_sec | integer | Seconds between checks, 10 to 86400 (push: expected period, up to 7 days). 0 = default (60, push 3600). |
| timeout_ms | integer | 500 to 60000. 0 = 10000. |
| retries | integer | Consecutive failures before a location counts as down, 1 to 10. |
| method | string | HTTP method for http/keyword (default GET). For mcp: the transport, auto, streamable or sse. |
| expected_status | string | Accepted HTTP status codes, e.g. "200-299" or "200,301". For mcp: the minimum number of tools. |
| keyword | string | keyword: text that must appear in the body. mcp: comma separated tool names that must be listed. |
| keyword_invert | boolean | keyword: fail when the text is found. |
| dns_record | string | dns: record type (A, AAAA, CNAME, MX, TXT, NS...). |
| dns_server | string | dns: resolver to query, host[:port]. Empty = system resolver. |
| dns_expected | string | dns: value that must be in the answer. |
| ignore_tls | boolean | Accept invalid TLS certificates. |
| ip_family | integer | 0 = instance default (GeneralSettings.ip_family), 1 = IPv4 or IPv6 (whichever connects first), 4 = IPv4 only, 6 = IPv6 only, 46 = IPv4 and IPv6 (both checked, the check fails when either fails). Locations without IPv6 check over IPv4 with 0 and 1; 6 and 46 are refused on them (see Location.ipv6). Ignored for push and docker.one of: 0 1 4 6 46 |
| headers | object | Request headers (http, keyword, mcp). |
| body | string | Request body (http, keyword). |
| assertions | Assertion[] | JSON response rules, http and keyword only, at most 20. |
| grace_sec | integer | push: extra time after the period before the monitor goes down, 0 to 86400. |
| degraded_pct | integer | Share of failing locations (percent) from which the monitor is degraded, 1 to 50. 0 = instance default. |
| locations | string[] | Where the check runs: "local" (the hub) and/or "agent:<id>". Empty = local. See GET /api/locations. |
| group_id | integer | Group, 0 = none. |
| tag_ids | integer[] | |
| notifiers | ChannelLink[] | Notification channels linked to this monitor one by one (notifier_id + events). Channels whose scope covers the monitor need no link. Left out on update: the links are kept; [] removes them. |
| cert_expiry_alert | boolean | Alert before the TLS certificate expires (https targets). |
| domain_expiry_alert | boolean | Alert before the domain registration expires. |
| paused | boolean | Create the monitor paused. Ignored on update: use POST /api/monitors/{id}/pause. |
{"name": "Website","type": "http","target": "https://example.com","interval_sec": 60,"timeout_ms": 10000,"retries": 2,"method": "GET","expected_status": "200-299","locations": ["local"],"tag_ids": [],"group_id": 0,"assertions": [{"path": "status","op": "eq","value": "ok"}],"cert_expiry_alert": true}
response 200 · Updated.
{"name": "Website","type": "http","target": "https://example.com","interval_sec": 60,"timeout_ms": 10000,"retries": 2,"method": "GET","expected_status": "200-299","locations": ["local"],"tag_ids": [],"group_id": 0,"assertions": [{"path": "status","op": "eq","value": "ok"}],"cert_expiry_alert": true,"id": 7,"headers": {},"body": "","keyword": "","keyword_invert": false,"dns_record": "","dns_server": "","dns_expected": "","ignore_tls": false,"push_token": "","grace_sec": 0,"degraded_pct": 0,"ip_family": 0,"domain_expiry_alert": false,"paused": false,"status": "up","status_since": 1761868800000,"created_at": 1761868800000}
/api/monitors/{id}Delete a monitorwrite token · adminAlso deletes its history.
| id* | path · integer | Id of the monitor. |
response 200 · Done.
{"ok": true}
/api/monitors/{id}/expiryCheck expiry dates nowwrite token · adminLooks up the TLS certificate and domain expiry.
| id* | path · integer | Id of the monitor. |
response 200 · Expiry checks.
[{"monitor_id": 1,"kind": "cert","subject": "string","expires_at": 1761868800000,"issuer": "string","source": "tls","error": "string","checked_at": 1761868800000}]
/api/monitors/{id}/pausePause or resumewrite token · adminPausing resolves the open incident.
| id* | path · integer | Id of the monitor. |
| paused* | boolean |
{"paused": true}
response 200 · Monitor.
{"name": "string","type": "http","target": "https://example.com/health","interval_sec": 1,"timeout_ms": 1,"retries": 1,"method": "string","expected_status": "string","keyword": "string","keyword_invert": true,"dns_record": "string","dns_server": "string","dns_expected": "string","ignore_tls": true,"ip_family": 0,"headers": {"key": "string"},"body": "string","assertions": [{"path": null,"op": null,"value": null}],"grace_sec": 1,"degraded_pct": 1,"locations": ["local","agent:2"],"group_id": 1,"tag_ids": [1],"notifiers": [{"monitor_id": null,"notifier_id": null,"events": null}],"cert_expiry_alert": true,"domain_expiry_alert": true,"paused": true,"id": 1,"push_token": "string","status": "up","status_since": 1761868800000,"created_at": 1761868800000}
/api/monitors/{id}/push-tokenRotate the heartbeat URLwrite token · adminpush monitors only: the old URL stops working.
| id* | path · integer | Id of the monitor. |
response 200 · Monitor with the new push_token.
{"name": "string","type": "http","target": "https://example.com/health","interval_sec": 1,"timeout_ms": 1,"retries": 1,"method": "string","expected_status": "string","keyword": "string","keyword_invert": true,"dns_record": "string","dns_server": "string","dns_expected": "string","ignore_tls": true,"ip_family": 0,"headers": {"key": "string"},"body": "string","assertions": [{"path": null,"op": null,"value": null}],"grace_sec": 1,"degraded_pct": 1,"locations": ["local","agent:2"],"group_id": 1,"tag_ids": [1],"notifiers": [{"monitor_id": null,"notifier_id": null,"events": null}],"cert_expiry_alert": true,"domain_expiry_alert": true,"paused": true,"id": 1,"push_token": "string","status": "up","status_since": 1761868800000,"created_at": 1761868800000}
/api/monitors/{id}/resultsLatest check resultsread token| id* | path · integer | Id of the monitor. |
| limit | query · integer | Number of results. |
| before | query · integer | Only results older than this time (unix ms), to load the next page. |
| from | query · integer | Only results at or after this time (unix ms). |
| to | query · integer | Only results before this time (unix ms). |
| failed | query · boolean | Only failed checks. |
response 200 · Results, newest first.
{"results": [{"id": 1,"monitor_id": 1,"location": "string","ts": 1761868800000,"ok": true,"latency_ms": 12.5,"status_code": 1,"message": "string","cert_expiry": 1,"maint": true,"timing": {"dns_ms": 12.5,"connect_ms": 12.5,"tls_ms": 12.5,"ttfb_ms": 12.5,"total_ms": 12.5,"n": 1}}],"locations": {"key": "string"}}
/api/monitors/{id}/seriesResponse time seriesread tokenAbout 90 buckets per location over the range, or over a custom window with start and end. stats sums up the window and the one of the same length just before it.
| id* | path · integer | Id of the monitor. |
| range | query · string | Time range.one of: 1h 6h 24h 7d 30d |
| start | query · integer | Custom window start, unix ms. With end, replaces range; 5 minutes to 400 days long. |
| end | query · integer | Custom window end, unix ms (capped at now). |
response 200 · Series.
{"from": 1761868800000,"to": 1761868800000,"size": 1,"locations": [{"id": "string","name": "string"}],"buckets": [{"t": 1761868800000,"location": "string","total": 1,"ok": 1,"degraded": 1,"maint": 1,"avg_ms": 12.5,"min_ms": 12.5,"max_ms": 12.5,"phases": {"dns_ms": 12.5,"connect_ms": 12.5,"tls_ms": 12.5,"ttfb_ms": 12.5,"total_ms": 12.5,"n": 1}}],"stats": {"current": {"from": 1,"to": 1,"uptime": 12.5,"avg_ms": 12.5,"percentiles": {"n": 1,"from": 1,"p50": 12.5,"p95": 12.5,"p99": 12.5}},"previous": {"from": 1,"to": 1,"uptime": 12.5,"avg_ms": 12.5,"percentiles": {"n": 1,"from": 1,"p50": 12.5,"p95": 12.5,"p99": 12.5}}}}
/api/monitors/{id}/testCheck nowwrite token · adminRuns the saved monitor's check once at each of its locations and returns the results. They are not stored and do not change the status.
| id* | path · integer | Id of the monitor. |
response 200 · One result per location of the monitor. Nothing is stored and no alert is sent.
{"results": [{"location": "local","name": "hub","ok": true,"latency_ms": 84.2,"status_code": 200},{"location": "agent:2","name": "fra · Frankfurt","ok": false,"skipped": true,"latency_ms": 0,"message": "agent offline"}]}
/api/monitors/bulkChange several monitorswrite token · adminPauses, resumes, deletes or moves to a group several monitors at once. Pausing resolves their open incidents. With action group, group_id 0 removes the monitors from their group.
| ids* | integer[] | |
| action* | string | one of: pause resume delete group |
| group_id | integer | Action group only: the group, 0 for none. |
{"ids": [1,2,3],"action": "pause"}
response 200 · Number of monitors selected.
{"count": 3}
/api/monitors/testTest an unsaved monitorwrite token · adminRuns the check once at each location of the monitor in the body and waits for the results (up to the timeout plus 5 seconds). Nothing is saved. Heartbeat (push) monitors cannot be tested.
| name* | string | |
| type* | string | one of: http keyword tcp ping dns push postgres mysql redis mongodb docker grpc smtp mcp |
| target | string | What to check, depends on the type: an http(s) URL (http, keyword, mcp), host:port (tcp, IPv6 in brackets: [2001:db8::1]:443), a hostname or IP (ping, dns), a connection URL (postgres://, mysql://, redis://, mongodb://), a container name or id (docker), grpc[s]://host:port[/service], smtp[s]://host[:port]. Empty for push. |
| interval_sec | integer | Seconds between checks, 10 to 86400 (push: expected period, up to 7 days). 0 = default (60, push 3600). |
| timeout_ms | integer | 500 to 60000. 0 = 10000. |
| retries | integer | Consecutive failures before a location counts as down, 1 to 10. |
| method | string | HTTP method for http/keyword (default GET). For mcp: the transport, auto, streamable or sse. |
| expected_status | string | Accepted HTTP status codes, e.g. "200-299" or "200,301". For mcp: the minimum number of tools. |
| keyword | string | keyword: text that must appear in the body. mcp: comma separated tool names that must be listed. |
| keyword_invert | boolean | keyword: fail when the text is found. |
| dns_record | string | dns: record type (A, AAAA, CNAME, MX, TXT, NS...). |
| dns_server | string | dns: resolver to query, host[:port]. Empty = system resolver. |
| dns_expected | string | dns: value that must be in the answer. |
| ignore_tls | boolean | Accept invalid TLS certificates. |
| ip_family | integer | 0 = instance default (GeneralSettings.ip_family), 1 = IPv4 or IPv6 (whichever connects first), 4 = IPv4 only, 6 = IPv6 only, 46 = IPv4 and IPv6 (both checked, the check fails when either fails). Locations without IPv6 check over IPv4 with 0 and 1; 6 and 46 are refused on them (see Location.ipv6). Ignored for push and docker.one of: 0 1 4 6 46 |
| headers | object | Request headers (http, keyword, mcp). |
| body | string | Request body (http, keyword). |
| assertions | Assertion[] | JSON response rules, http and keyword only, at most 20. |
| grace_sec | integer | push: extra time after the period before the monitor goes down, 0 to 86400. |
| degraded_pct | integer | Share of failing locations (percent) from which the monitor is degraded, 1 to 50. 0 = instance default. |
| locations | string[] | Where the check runs: "local" (the hub) and/or "agent:<id>". Empty = local. See GET /api/locations. |
| group_id | integer | Group, 0 = none. |
| tag_ids | integer[] | |
| notifiers | ChannelLink[] | Notification channels linked to this monitor one by one (notifier_id + events). Channels whose scope covers the monitor need no link. Left out on update: the links are kept; [] removes them. |
| cert_expiry_alert | boolean | Alert before the TLS certificate expires (https targets). |
| domain_expiry_alert | boolean | Alert before the domain registration expires. |
| paused | boolean | Create the monitor paused. Ignored on update: use POST /api/monitors/{id}/pause. |
{"type": "http","target": "https://example.com/health","timeout_ms": 10000,"locations": ["local"]}
response 200 · One result per location of the monitor. Nothing is stored and no alert is sent.
{"results": [{"location": "local","name": "hub","ok": true,"latency_ms": 84.2,"status_code": 200},{"location": "agent:2","name": "fra · Frankfurt","ok": false,"skipped": true,"latency_ms": 0,"message": "agent offline"}]}
/api/push/{token}Send a heartbeatpublicHeartbeat of a push monitor. Any HTTP method works (GET and POST shown here). The token in the URL is the authentication.
curl -fsS https://status.example.com/api/push/<token>?status=up&msg=ok&ping=12
| token* | path · string | |
| status | query · string | Result of the job.one of: up down ok fail 0 1 |
| msg | query · string | Message shown in the history, up to 200 characters. |
| ping | query · number | Duration or latency in ms. |
response 200 · Recorded.
{"ok": true}
/api/push/{token}Send a heartbeatpublicHeartbeat of a push monitor. Any HTTP method works (GET and POST shown here). The token in the URL is the authentication.
curl -fsS https://status.example.com/api/push/<token>?status=up&msg=ok&ping=12
| token* | path · string | |
| status | query · string | Result of the job.one of: up down ok fail 0 1 |
| msg | query · string | Message shown in the history, up to 200 characters. |
| ping | query · number | Duration or latency in ms. |
response 200 · Recorded.
{"ok": true}
──incidents
Down periods of monitors.
/api/incidentsList incidentsread tokenThe latest 100, open ones first.
| monitor | query · integer | Only this monitor. |
| state | query · string | Only open or only resolved incidents.one of: open resolved |
| since | query · integer | Only incidents open at some point after this time (unix ms). |
| limit | query · integer | Maximum number of incidents. |
response 200 · Incidents.
[{"id": 1,"monitor_id": 1,"monitor_name": "string","started_at": 1761868800000,"resolved_at": 1,"cause": "string"}]
──maintenance
Windows during which monitors don't alert.
/api/maintenancesList maintenance windowsread tokenresponse 200 · Windows.
[{"name": "string","description": "string","all_monitors": true,"monitor_ids": [1],"group_ids": [1],"tag_ids": [1],"kind": "once","start_at": 1,"end_at": 1,"start_time": "02:00","duration_min": 1,"weekdays": [1],"month_days": [1],"timezone": "Europe/Rome","enabled": true,"id": 1,"created_at": 1761868800000,"active": true,"active_end": 1761868800000,"next_start": 1,"next_end": 1761868800000}]
/api/maintenancesCreate a maintenance windowwrite token · adminCovered monitors don't alert and don't count as down.
| name* | string | |
| description | string | |
| all_monitors | boolean | Covers every monitor. |
| monitor_ids | integer[] | |
| group_ids | integer[] | Every monitor of these groups. |
| tag_ids | integer[] | Every monitor with one of these tags. |
| kind* | string | one of: once daily weekly monthly |
| start_at | integer | once: start. Recurring: first day the schedule is valid (0 = now). |
| end_at | integer | once: end. Recurring: last day the schedule is valid (0 = forever). |
| start_time | string | Recurring: local start time, HH:MM. |
| duration_min | integer | Recurring: length in minutes, up to 7 days. |
| weekdays | integer[] | weekly: 0 = Sunday … 6 = Saturday. |
| month_days | integer[] | monthly: 1 … 31. |
| timezone | string | IANA time zone, default UTC. |
| enabled | boolean |
{"name": "DB upgrade","description": "","all_monitors": false,"monitor_ids": [7],"group_ids": [],"tag_ids": [],"kind": "weekly","start_time": "02:00","duration_min": 60,"weekdays": [0],"month_days": [],"timezone": "Europe/Rome","enabled": true}
response 201 · Created.
{"name": "string","description": "string","all_monitors": true,"monitor_ids": [1],"group_ids": [1],"tag_ids": [1],"kind": "once","start_at": 1,"end_at": 1,"start_time": "02:00","duration_min": 1,"weekdays": [1],"month_days": [1],"timezone": "Europe/Rome","enabled": true,"id": 1,"created_at": 1761868800000,"active": true,"active_end": 1761868800000,"next_start": 1,"next_end": 1761868800000}
/api/maintenances/{id}Update a maintenance windowwrite token · admin| id* | path · integer | Id of the maintenance window. |
| name* | string | |
| description | string | |
| all_monitors | boolean | Covers every monitor. |
| monitor_ids | integer[] | |
| group_ids | integer[] | Every monitor of these groups. |
| tag_ids | integer[] | Every monitor with one of these tags. |
| kind* | string | one of: once daily weekly monthly |
| start_at | integer | once: start. Recurring: first day the schedule is valid (0 = now). |
| end_at | integer | once: end. Recurring: last day the schedule is valid (0 = forever). |
| start_time | string | Recurring: local start time, HH:MM. |
| duration_min | integer | Recurring: length in minutes, up to 7 days. |
| weekdays | integer[] | weekly: 0 = Sunday … 6 = Saturday. |
| month_days | integer[] | monthly: 1 … 31. |
| timezone | string | IANA time zone, default UTC. |
| enabled | boolean |
{"name": "DB upgrade","description": "","all_monitors": false,"monitor_ids": [7],"group_ids": [],"tag_ids": [],"kind": "weekly","start_time": "02:00","duration_min": 60,"weekdays": [0],"month_days": [],"timezone": "Europe/Rome","enabled": true}
response 200 · Updated.
{"name": "string","description": "string","all_monitors": true,"monitor_ids": [1],"group_ids": [1],"tag_ids": [1],"kind": "once","start_at": 1,"end_at": 1,"start_time": "02:00","duration_min": 1,"weekdays": [1],"month_days": [1],"timezone": "Europe/Rome","enabled": true,"id": 1,"created_at": 1761868800000,"active": true,"active_end": 1761868800000,"next_start": 1,"next_end": 1761868800000}
/api/maintenances/{id}Delete a maintenance windowwrite token · admin| id* | path · integer | Id of the maintenance window. |
response 200 · Done.
{"ok": true}
──groups and tags
Organize monitors; status pages and maintenance windows can select them by group or tag.
/api/groupsList groupsread tokenresponse 200 · Groups.
[{"id": 1,"name": "string","description": "string","monitors": 1,"created_at": 1761868800000}]
/api/groupsCreate a groupwrite token · admin| id | integer | |
| name* | string | Unique, up to 64 characters. |
| description | string | Up to 500 characters. |
| monitors | integer | Number of monitors in the group (read only). |
| created_at | integer | Unix time in milliseconds (0 = never / unknown). |
{"name": "Production","description": "Customer facing"}
response 201 · Created.
{"id": 1,"name": "string","description": "string","monitors": 1,"created_at": 1761868800000}
/api/groups/{id}Update a groupwrite token · admin| id* | path · integer | Id of the group. |
| id | integer | |
| name* | string | Unique, up to 64 characters. |
| description | string | Up to 500 characters. |
| monitors | integer | Number of monitors in the group (read only). |
| created_at | integer | Unix time in milliseconds (0 = never / unknown). |
{"id": 1,"name": "string","description": "string","monitors": 1,"created_at": 1761868800000}
response 200 · Updated.
{"id": 1,"name": "string","description": "string","monitors": 1,"created_at": 1761868800000}
/api/groups/{id}Delete a groupwrite token · adminIts monitors are kept, without a group.
| id* | path · integer | Id of the group. |
response 200 · Done.
{"ok": true}
/api/groups/{id}/monitorsSet the monitors of a groupwrite token · adminMonitors listed here move to this group; monitors of the group not listed leave it.
| id* | path · integer | Id of the group. |
| monitor_ids* | integer[] |
{"monitor_ids": [7,8]}
response 200 · Group.
{"id": 1,"name": "string","description": "string","monitors": 1,"created_at": 1761868800000}
/api/tagsList tagsread tokenresponse 200 · Tags.
[{"id": 1,"name": "string","color": "red","monitors": 1,"created_at": 1761868800000}]
/api/tagsCreate a tagwrite token · admin| id | integer | |
| name* | string | Unique, up to 32 characters, no commas. |
| color | string | one of: "" red orange yellow green cyan blue purple pink gray |
| monitors | integer | Number of tagged monitors (read only). |
| created_at | integer | Unix time in milliseconds (0 = never / unknown). |
{"name": "critical","color": "red"}
response 201 · Created.
{"id": 1,"name": "string","color": "red","monitors": 1,"created_at": 1761868800000}
/api/tags/{id}Update a tagwrite token · admin| id* | path · integer | Id of the tag. |
| id | integer | |
| name* | string | Unique, up to 32 characters, no commas. |
| color | string | one of: "" red orange yellow green cyan blue purple pink gray |
| monitors | integer | Number of tagged monitors (read only). |
| created_at | integer | Unix time in milliseconds (0 = never / unknown). |
{"id": 1,"name": "string","color": "red","monitors": 1,"created_at": 1761868800000}
response 200 · Updated.
{"id": 1,"name": "string","color": "red","monitors": 1,"created_at": 1761868800000}
/api/tags/{id}Delete a tagwrite token · admin| id* | path · integer | Id of the tag. |
response 200 · Done.
{"ok": true}
──agents
Remote check locations connected to the hub.
/api/agentsList agentsread tokenresponse 200 · Agents.
{"agents": [{"id": 1,"name": "string","location": "string","version": "string","hostname": "string","platform": "string","remote_addr": "string","last_seen": 1761868800000,"created_at": 1761868800000,"ipv6": 0,"location_id": "agent:2","online": true,"monitors": 1,"update_available": true,"self_update": true,"no_update_reason": "string","update_state": "updating","update_error": "string"}],"hub_url": "string","hub_version": "string","auto_update": true}
/api/agentsCreate an agentwrite token · adminThe agent token is returned once.
| name* | string | |
| location | string |
{"name": "fra-1","location": "Frankfurt"}
response 201 · Created.
{"agent": {"id": 1,"name": "string","location": "string","version": "string","hostname": "string","platform": "string","remote_addr": "string","last_seen": 1761868800000,"created_at": 1761868800000,"ipv6": 0,"location_id": "agent:2","online": true,"monitors": 1,"update_available": true,"self_update": true,"no_update_reason": "string","update_state": "updating","update_error": "string"},"token": "string","hub_url": "string"}
/api/agents/{id}Rename an agentwrite token · admin| id* | path · integer | Id of the agent. |
| name* | string | |
| location | string |
{"name": "string","location": "string"}
response 200 · Done.
{"ok": true}
/api/agents/{id}Delete an agentwrite token · adminDisconnects it and removes it from monitor locations.
| id* | path · integer | Id of the agent. |
response 200 · Done.
{"ok": true}
/api/agents/{id}/monitorsPreview compatible monitorsread tokenMonitors this agent can be added to, and the ones skipped with the reason. Heartbeat monitors have no locations and are left out.
| id* | path · integer | Id of the agent. |
response 200 · Compatible and skipped monitors.
{"compatible": [{"id": 1,"name": "string","type": "string","target": "string","reason": "no_ipv6"}],"skipped": [{"id": 1,"name": "string","type": "string","target": "string","reason": "no_ipv6"}],"already": 1}
/api/agents/{id}/monitorsAdd the agent to monitorswrite token · adminAdds the agent's location to the given monitors. Monitors that are not compatible are skipped and returned with the reason.
| id* | path · integer | Id of the agent. |
| monitor_ids* | integer[] |
{"monitor_ids": [1,4,7]}
response 200 · Done.
{"added": 1,"skipped": [{"id": 1,"name": "string","type": "string","target": "string","reason": "no_ipv6"}]}
/api/agents/{id}/tokenRotate the agent tokenwrite token · adminThe agent is disconnected until it uses the new token.
| id* | path · integer | Id of the agent. |
response 200 · New token.
{"token": "string","hub_url": "string"}
/api/agents/{id}/updateUpdate an agent nowwrite token · adminAsks a connected agent to install the hub's version.
| id* | path · integer | Id of the agent. |
response 200 · Done.
{"ok": true}
/api/locationsList check locationsread tokenThe hub (when it runs checks) and every agent.
response 200 · Locations.
[{"id": "local","name": "hub","online": true,"kind": "local","ipv6": 2},{"id": "agent:2","name": "fra-1 · Frankfurt","online": true,"kind": "agent","ipv6": 1}]
/api/settings/agent-updatesAgent auto-update settingwrite token · admin| auto_update* | boolean |
{"auto_update": true}
response 200 · Saved.
{"auto_update": true}
──notifications
Alert channels: webhook, Discord, Slack, Telegram, ntfy, email.
/api/mobile/configPush config for the mobile appread tokenWhere the app enrolls its phone: it sends hub_id and its FCM token to {relay_url}/v1/devices and gives the handle it gets to POST /api/mobile/devices. live_activity is true when the hub also pushes Live Activity updates (PUT /api/mobile/live-activity/...). enabled is false (and the other fields empty) when push is off.
response 200 · Config.
{"enabled": true,"relay_url": "https://push.pingtxt.com","hub_id": "PrBlwRm8tbClvg8f5PEy4w","live_activity": true}
/api/mobile/devicesList my phonesread tokenresponse 200 · Phones of the current user.
[{"id": 1,"name": "string","created_at": 1761868800000,"last_seen_at": 1761868800000}]
/api/mobile/devicesRegister my phoneread tokenStores the device handle (prd_...) from the push relay for the app's sign-in. Only OAuth access tokens (pma_...) can call it; each sign-in keeps one handle, a new one replaces it. Revoking the sign-in removes the phone.
| device* | string | |
| name | string |
{"device": "prd_AeYWsf5y...","name": "Federico's iPhone"}
response 200 · Saved.
{"id": 3}
/api/mobile/devices/{id}Remove my phoneread tokenThe phone stops getting notifications from this hub.
| id* | path · integer | Id of the phone. |
response 200 · Done.
{"ok": true}
/api/mobile/live-activity/start-tokenSet my Live Activity push-to-start tokenread tokenStores the ActivityKit push-to-start token of the app (iOS 17.2+, Activity<OutageActivityAttributes>.pushToStartTokenUpdates) and the hub URL the app signed in with. When a monitor goes down and this phone has no activity running, the hub starts one through the push relay with attributes {"hub": hub_url}. Send it again whenever iOS gives a new one.
| token* | string | |
| hub_url* | string |
{"token": "80f4c3...","hub_url": "https://status.example.com"}
response 200 · Saved.
{"ok": true}
/api/mobile/live-activity/start-tokenRemove my Live Activity push-to-start tokenread tokenThe hub no longer starts the activity on this phone (for example, Live Activities turned off in the app).
response 200 · Saved.
{"ok": true}
/api/mobile/live-activity/tokenSet the token of my running Live Activityread tokenStores the push token of the activity running on this phone (activity.pushTokenUpdates). The hub sends it update and end pushes when incidents open or close, and refreshes it every 15 minutes while incidents stay open. Send it again when it rotates.
| token* | string |
{"token": "6a0d1e..."}
response 200 · Saved.
{"ok": true}
/api/mobile/live-activity/tokenRemove the token of my Live Activityread tokenCall it when the activity ends on the phone (ended by the app or dismissed by the user).
response 200 · Saved.
{"ok": true}
/api/notifiersList notification channelsread tokenresponse 200 · Channels.
[{"id": 1,"name": "string","type": "webhook","config": {"key": "string"},"enabled": true,"delay_min": 1,"repeat_min": 1,"all_monitors": true,"group_ids": [1],"tag_ids": [1],"events": ["down"],"monitors": [{"monitor_id": 1,"notifier_id": 1,"events": [null]}],"created_at": 1761868800000}]
/api/notifiersCreate a notification channelwrite token · admin| id | integer | |
| name* | string | |
| type* | string | one of: webhook discord slack telegram ntfy email app |
| config* | object | webhook, discord, slack, ntfy: {url}. telegram: {bot_token, chat_id}. email: {host, port, security (starttls|tls|none), username, password, from, to}. app: {users}, emails of the users whose phones get the alerts, comma separated (empty = every user). The email password is write-only: responses carry password_set="true" instead, and an empty password on update keeps the stored one. Read-only users see secrets as ********. |
| enabled | boolean | |
| delay_min | integer | Minutes a monitor must stay down before this channel gets its first alert (0 = right away). Use it for an escalation contact: shorter outages never reach it, and it gets the recovery message only if it was alerted. |
| repeat_min | integer | Repeat the alert every this many minutes while the monitor stays down (0 = once, otherwise at least 5). Reminders carry reminder=1, 2, ... and down_since in the webhook payload. Nothing is sent during maintenance windows. |
| all_monitors | boolean | The channel covers every monitor, also the ones added later. Default true when left out on create. |
| group_ids | integer[] | Without all_monitors: the channel covers the monitors of these groups. |
| tag_ids | integer[] | Without all_monitors: the channel covers the monitors with one of these tags. |
| events | "down" | "up" | "degraded" | "cert_expiry" | "domain_expiry"[] | What the channel gets for the monitors it covers. Default on create: down, up, cert_expiry, domain_expiry. down = the monitor went down (and the reminders of repeat_min), up = it is back up (only to channels that got the down alert), degraded = some locations fail and when that ends, cert_expiry / domain_expiry = the TLS certificate or the domain registration expires soon, expired or was renewed. |
| monitors | ChannelLink[] | Monitors linked one by one (monitor_id + events), the same links as the monitors' notifiers. Left out on update: kept; [] removes them. |
| created_at | integer | Unix time in milliseconds (0 = never / unknown). |
{"name": "ops","type": "slack","config": {"url": "https://hooks.slack.com/services/T000/B000/XXXX"},"enabled": true}
response 201 · Created.
{"id": 1,"name": "string","type": "webhook","config": {"key": "string"},"enabled": true,"delay_min": 1,"repeat_min": 1,"all_monitors": true,"group_ids": [1],"tag_ids": [1],"events": ["down"],"monitors": [{"monitor_id": 1,"notifier_id": 1,"events": ["down"]}],"created_at": 1761868800000}
/api/notifiers/{id}Update a notification channelwrite token · admin| id* | path · integer | Id of the channel. |
| id | integer | |
| name* | string | |
| type* | string | one of: webhook discord slack telegram ntfy email app |
| config* | object | webhook, discord, slack, ntfy: {url}. telegram: {bot_token, chat_id}. email: {host, port, security (starttls|tls|none), username, password, from, to}. app: {users}, emails of the users whose phones get the alerts, comma separated (empty = every user). The email password is write-only: responses carry password_set="true" instead, and an empty password on update keeps the stored one. Read-only users see secrets as ********. |
| enabled | boolean | |
| delay_min | integer | Minutes a monitor must stay down before this channel gets its first alert (0 = right away). Use it for an escalation contact: shorter outages never reach it, and it gets the recovery message only if it was alerted. |
| repeat_min | integer | Repeat the alert every this many minutes while the monitor stays down (0 = once, otherwise at least 5). Reminders carry reminder=1, 2, ... and down_since in the webhook payload. Nothing is sent during maintenance windows. |
| all_monitors | boolean | The channel covers every monitor, also the ones added later. Default true when left out on create. |
| group_ids | integer[] | Without all_monitors: the channel covers the monitors of these groups. |
| tag_ids | integer[] | Without all_monitors: the channel covers the monitors with one of these tags. |
| events | "down" | "up" | "degraded" | "cert_expiry" | "domain_expiry"[] | What the channel gets for the monitors it covers. Default on create: down, up, cert_expiry, domain_expiry. down = the monitor went down (and the reminders of repeat_min), up = it is back up (only to channels that got the down alert), degraded = some locations fail and when that ends, cert_expiry / domain_expiry = the TLS certificate or the domain registration expires soon, expired or was renewed. |
| monitors | ChannelLink[] | Monitors linked one by one (monitor_id + events), the same links as the monitors' notifiers. Left out on update: kept; [] removes them. |
| created_at | integer | Unix time in milliseconds (0 = never / unknown). |
{"name": "ops","type": "slack","config": {"url": "https://hooks.slack.com/services/T000/B000/XXXX"},"enabled": true}
response 200 · Updated.
{"id": 1,"name": "string","type": "webhook","config": {"key": "string"},"enabled": true,"delay_min": 1,"repeat_min": 1,"all_monitors": true,"group_ids": [1],"tag_ids": [1],"events": ["down"],"monitors": [{"monitor_id": 1,"notifier_id": 1,"events": ["down"]}],"created_at": 1761868800000}
/api/notifiers/{id}Delete a notification channelwrite token · admin| id* | path · integer | Id of the channel. |
response 200 · Done.
{"ok": true}
/api/notifiers/{id}/testTest a channelwrite token · admin| id* | path · integer | Id of the channel. |
response 200 · Done.
{"ok": true}
/api/notifiers/testTest an unsaved channelwrite token · adminSends a test alert with this configuration. With an id, empty secrets are taken from that saved channel.
| id | integer | |
| name* | string | |
| type* | string | one of: webhook discord slack telegram ntfy email app |
| config* | object | webhook, discord, slack, ntfy: {url}. telegram: {bot_token, chat_id}. email: {host, port, security (starttls|tls|none), username, password, from, to}. app: {users}, emails of the users whose phones get the alerts, comma separated (empty = every user). The email password is write-only: responses carry password_set="true" instead, and an empty password on update keeps the stored one. Read-only users see secrets as ********. |
| enabled | boolean | |
| delay_min | integer | Minutes a monitor must stay down before this channel gets its first alert (0 = right away). Use it for an escalation contact: shorter outages never reach it, and it gets the recovery message only if it was alerted. |
| repeat_min | integer | Repeat the alert every this many minutes while the monitor stays down (0 = once, otherwise at least 5). Reminders carry reminder=1, 2, ... and down_since in the webhook payload. Nothing is sent during maintenance windows. |
| all_monitors | boolean | The channel covers every monitor, also the ones added later. Default true when left out on create. |
| group_ids | integer[] | Without all_monitors: the channel covers the monitors of these groups. |
| tag_ids | integer[] | Without all_monitors: the channel covers the monitors with one of these tags. |
| events | "down" | "up" | "degraded" | "cert_expiry" | "domain_expiry"[] | What the channel gets for the monitors it covers. Default on create: down, up, cert_expiry, domain_expiry. down = the monitor went down (and the reminders of repeat_min), up = it is back up (only to channels that got the down alert), degraded = some locations fail and when that ends, cert_expiry / domain_expiry = the TLS certificate or the domain registration expires soon, expired or was renewed. |
| monitors | ChannelLink[] | Monitors linked one by one (monitor_id + events), the same links as the monitors' notifiers. Left out on update: kept; [] removes them. |
| created_at | integer | Unix time in milliseconds (0 = never / unknown). |
{"name": "ops","type": "slack","config": {"url": "https://hooks.slack.com/services/T000/B000/XXXX"},"enabled": true}
response 200 · Done.
{"ok": true}
──reports
Periodic email reports: uptime and incidents of the last week or month, per monitor group.
/api/reportsGet report settingsread tokenReport settings, when the next reports go out and the last ones sent.
response 200 · Settings.
{"weekly": true,"monthly": true,"hour": 1,"timezone": "Europe/Rome","notifier_ids": [1],"group_ids": [1],"next": {"key": 1},"last": {"key": {"from": 1,"sent_at": 1}}}
/api/reportsSave report settingswrite token · adminTurns the weekly and monthly reports on or off and picks the email channels, the groups, the hour and the time zone. A report that is turned on starts with the next period.
| weekly | boolean | Send a report every Monday for the week before. |
| monthly | boolean | Send a report on the 1st for the month before. |
| hour | integer | Hour of the day the reports are sent, in timezone. |
| timezone | string | IANA time zone of the periods and of the times in the report. |
| notifier_ids | integer[] | Email channels that receive the reports. Disabled channels are skipped. |
| group_ids | integer[] | Only these monitor groups. Empty: every monitor. |
{"weekly": true,"monthly": true,"hour": 8,"timezone": "Europe/Rome","notifier_ids": [3],"group_ids": []}
response 200 · Saved.
{"weekly": true,"monthly": true,"hour": 1,"timezone": "Europe/Rome","notifier_ids": [1],"group_ids": [1],"next": {"key": 1},"last": {"key": {"from": 1,"sent_at": 1}}}
/api/reports/previewPreview a reportread tokenThe report of the last complete week (Monday to Sunday) or month, with the saved settings, as it would be sent now. text is the plain text body of the email.
| kind | query · string | Which report.one of: weekly monthly |
response 200 · Report.
{"kind": "weekly","from": 1,"to": 1,"timezone": "string","uptime": 12.5,"prev_uptime": 12.5,"incidents": 1,"downtime_ms": 1,"monitors": 1,"groups": [{"id": 1,"name": "string","uptime": 12.5,"prev_uptime": 12.5,"incidents": 1,"downtime_ms": 1,"monitors": [null],"incident_list": [null]}],"subject": "string","text": "string","url": "string"}
/api/reports/sendSend a report nowwrite token · adminEmails the report of the last complete period to the saved channels now. The schedule does not change.
| kind | string | one of: weekly monthly |
{"kind": "monthly"}
response 200 · Sent to at least one channel.
{"ok": true,"sent": 1}
──status pages
Public pages and their data.
/api/badge/{slug}/{kind}Status page badgepublicSVG badge with the overall state of a status page (operational, degraded, partial outage, major outage), to embed in a README:
``markdown

| slug* | path · string | Slug of a published status page. |
| kind* | path · string | status, optionally with .svg.one of: status status.svg |
| label | query · string | Replaces the left text (up to 60 characters). |
| style | query · string | flat (default) or flat-square.one of: flat flat-square |
/api/badge/{slug}/{monitor}/{kind}Monitor badgepublicSVG badge of one monitor shown on a published status page: its state (status), its uptime (uptime) or its average response time (response) over period. The label defaults to the monitor name, uptime 30d or response 24h.
``markdown

| slug* | path · string | Slug of a published status page. |
| monitor* | path · integer | Monitor id. |
| kind* | path · string | status, uptime or response, optionally with .svg.one of: status uptime response status.svg uptime.svg response.svg |
| period | query · string | Window for uptime (default 30d) and response (default 24h).one of: 24h 7d 30d 90d |
| label | query · string | Replaces the left text (up to 60 characters). |
| style | query · string | flat (default) or flat-square.one of: flat flat-square |
/api/public/status/{slug}Public status page datapublicWhat /status/<slug> shows. Drafts answer 404 unless a user is logged in. Cached for 15 seconds.
Published pages without a password answer with Access-Control-Allow-Origin: *, so any website can read them from the browser (for example to show a status dot).
| slug* | path · string |
response 200 · Page.
{"slug": "string","title": "string","description": "string","header_url": "string","logo": {"svg": true,"width": 1,"height": 1,"v": 1},"hide_title": true,"published": true,"status": "operational","days": 1,"monitors": [{"id": 1,"name": "string","group": "string","status": "string","uptime": 12.5,"uptime_24h": 12.5,"bars": [{"t": null,"total": null,"ok": null,"degraded": null,"maint": null,"avg_ms": null}]}],"show_incidents": true,"incidents": [{"id": 1,"monitor_name": "string","started_at": 1761868800000,"resolved_at": 1761868800000}],"updated_at": 1761868800000,"reports": [{"id": 1,"page_id": 1,"title": "string","impact": "none","status": "investigating","created_at": 1761868800000,"updated_at": 1761868800000,"resolved_at": 1,"updates": [null]}],"subscribe": true}
/api/public/status/{slug}/logo/{file}Status page logopublicOne format of the page logo: logo.svg, or logo.avif, logo.webp and logo.png. Same access rules as the page (drafts for logged-in users, password protected pages need the unlock cookie). With the current ?v= the file is cached for a year. On a custom domain the path is /api/public/status/logo/{file}.
| slug* | path · string | |
| file* | path · string | one of: logo.svg logo.avif logo.webp logo.png |
| v | query · integer | Logo version from the page JSON. |
/api/public/status/{slug}/subscribeSubscribe to a status page by emailpublicSends a confirmation link to the address; nothing else is sent until it is confirmed. The answer is the same for new, waiting and already confirmed addresses. On a custom domain the path is /api/public/status/subscribe. Limited to 10 requests per client address and 3 per email address every 15 minutes.
| slug* | path · string |
| email* | string |
{"email": "jane@example.com"}
response 202 · Confirmation email queued (or already subscribed).
{"ok": true}
/api/public/status/{slug}/unlockUnlock a password protected status pagepublicChecks the page password and sets a cookie that opens the page, its badges and its subscription form for 30 days. Changing or removing the password closes the page again for every visitor. On a custom domain the path is /api/public/status/unlock. Limited to 10 wrong passwords per client address and 100 per page every 15 minutes.
| slug* | path · string |
| password* | string |
{"password": "string"}
response 200 · Unlocked (or the page has no password); the cookie is set.
{"ok": true}
/api/public/subscriptions/{token}/confirmConfirm an email subscriptionpublicCalled by the status page when a visitor opens the link of the confirmation email. Links expire after 7 days.
| token* | path · string | Token of the links sent by email. |
response 200 · Confirmed.
{"ok": true,"email": "string","page": "string"}
/api/public/subscriptions/{token}/unsubscribeUnsubscribe from a status pagepublicRemoves the address. Also the target of the List-Unsubscribe header of update emails (RFC 8058 one-click: the body may be a form, not JSON).
| token* | path · string | Token of the links sent by email. |
response 200 · Unsubscribed.
{"ok": true,"email": "string","page": "string"}
/api/status-pagesList status pagesread tokenresponse 200 · Pages.
{"pages": [{"id": 1,"slug": "string","title": "string","description": "string","domain": "string","header_url": "string","hide_title": true,"logo": {"svg": true,"width": 1,"height": 1,"v": 1},"published": true,"show_incidents": true,"monitor_ids": [1],"group_ids": [1],"created_at": 1761868800000,"subscribe_notifier_id": 1,"protected": true,"password": "string"}],"hub_url": "string"}
/api/status-pagesCreate a status pagewrite token · admin| id | integer | |
| slug* | string | URL path: /status/<slug>. 1-50 lowercase letters, digits or dashes. |
| title* | string | |
| description | string | |
| domain | string | Optional custom domain serving the page, e.g. status.example.com. |
| header_url | string | Optional http(s) address the page name links to, e.g. the company website. Empty = the name is not a link. |
| hide_title | boolean | Show only the logo in the page header, without the page name (it stays the browser tab title). Ignored when the page has no logo. |
| logo | PageLogo | The page logo, only in the answer of GET /api/status-pages/{id}. Change it with the logo routes. |
| published | boolean | Drafts are visible only to logged-in users. |
| show_incidents | boolean | |
| monitor_ids | integer[] | |
| group_ids | integer[] | Every monitor of these groups, current and future. |
| created_at | integer | Unix time in milliseconds (0 = never / unknown). |
| subscribe_notifier_id | integer | Email notification channel that sends incident updates to the page's subscribers (its SMTP server and sender are used, its recipients are not). 0 = visitors cannot subscribe. |
| protected | boolean | Visitors need a password to open the page, its badges and its subscription form. Logged-in users open it without one. On save, false removes the password and true keeps the current one. |
| password | string | New password for visitors (6-72 characters), stored hashed and never returned. Needed when protected is set on a page without one. |
{"slug": "acme","title": "Acme status","description": "","domain": "","published": true,"show_incidents": true,"monitor_ids": [7],"group_ids": []}
response 201 · Created.
{"id": 1,"slug": "string","title": "string","description": "string","domain": "string","header_url": "string","hide_title": true,"logo": {"svg": true,"width": 1,"height": 1,"v": 1},"published": true,"show_incidents": true,"monitor_ids": [1],"group_ids": [1],"created_at": 1761868800000,"subscribe_notifier_id": 1,"protected": true,"password": "string"}
/api/status-pages/{id}Get a status pageread token| id* | path · integer | Id of the status page. |
response 200 · Page.
{"id": 1,"slug": "string","title": "string","description": "string","domain": "string","header_url": "string","hide_title": true,"logo": {"svg": true,"width": 1,"height": 1,"v": 1},"published": true,"show_incidents": true,"monitor_ids": [1],"group_ids": [1],"created_at": 1761868800000,"subscribe_notifier_id": 1,"protected": true,"password": "string"}
/api/status-pages/{id}Update a status pagewrite token · admin| id* | path · integer | Id of the status page. |
| id | integer | |
| slug* | string | URL path: /status/<slug>. 1-50 lowercase letters, digits or dashes. |
| title* | string | |
| description | string | |
| domain | string | Optional custom domain serving the page, e.g. status.example.com. |
| header_url | string | Optional http(s) address the page name links to, e.g. the company website. Empty = the name is not a link. |
| hide_title | boolean | Show only the logo in the page header, without the page name (it stays the browser tab title). Ignored when the page has no logo. |
| logo | PageLogo | The page logo, only in the answer of GET /api/status-pages/{id}. Change it with the logo routes. |
| published | boolean | Drafts are visible only to logged-in users. |
| show_incidents | boolean | |
| monitor_ids | integer[] | |
| group_ids | integer[] | Every monitor of these groups, current and future. |
| created_at | integer | Unix time in milliseconds (0 = never / unknown). |
| subscribe_notifier_id | integer | Email notification channel that sends incident updates to the page's subscribers (its SMTP server and sender are used, its recipients are not). 0 = visitors cannot subscribe. |
| protected | boolean | Visitors need a password to open the page, its badges and its subscription form. Logged-in users open it without one. On save, false removes the password and true keeps the current one. |
| password | string | New password for visitors (6-72 characters), stored hashed and never returned. Needed when protected is set on a page without one. |
{"slug": "acme","title": "Acme status","description": "","domain": "","published": true,"show_incidents": true,"monitor_ids": [7],"group_ids": []}
response 200 · Updated.
{"id": 1,"slug": "string","title": "string","description": "string","domain": "string","header_url": "string","hide_title": true,"logo": {"svg": true,"width": 1,"height": 1,"v": 1},"published": true,"show_incidents": true,"monitor_ids": [1],"group_ids": [1],"created_at": 1761868800000,"subscribe_notifier_id": 1,"protected": true,"password": "string"}
/api/status-pages/{id}Delete a status pagewrite token · admin| id* | path · integer | Id of the status page. |
response 200 · Done.
{"ok": true}
/api/status-pages/{id}/incidentsList the incidents of a status pageread tokenIncidents written on the page, open ones first, each with its updates (newest first).
| id* | path · integer | Id of the status page. |
response 200 · Incidents.
{"incidents": [{"id": 1,"page_id": 1,"title": "string","impact": "none","status": "investigating","created_at": 1761868800000,"updated_at": 1761868800000,"resolved_at": 1,"updates": [{"id": null,"incident_id": null,"status": null,"message": null,"created_at": null}]}]}
/api/status-pages/{id}/incidentsCreate an incident on a status pagewrite token · adminOpens an incident with its first update. With notify (default true) the update is emailed to the confirmed subscribers of the page, in the background.
| id* | path · integer | Id of the status page. |
| title | string | One line, at most 200 characters. Required to create and to edit. |
| impact | string | Default minor.one of: none minor major critical |
| status | string | Status of the update. Default investigating when creating.one of: investigating identified monitoring resolved |
| message | string | Text of the update, at most 5000 characters. Required to create and for updates. |
| notify | boolean | Email the update to the confirmed subscribers of the page (default true). Ignored when editing the title or impact. |
{"title": "Slow API responses","impact": "minor","status": "investigating","message": "We are looking into slow responses of the API.","notify": true}
response 201 · Created.
{"incident": {"id": 1,"page_id": 1,"title": "string","impact": "none","status": "investigating","created_at": 1761868800000,"updated_at": 1761868800000,"resolved_at": 1,"updates": [{"id": 1,"incident_id": 1,"status": "investigating","message": "string","created_at": 1761868800000}]},"notified": 1}
/api/status-pages/{id}/incidents/{incident}Edit an incidentwrite token · adminChanges the title and impact. Updates are added with POST …/updates.
| id* | path · integer | Id of the status page. |
| incident* | path · integer | Id of the incident. |
| title | string | One line, at most 200 characters. Required to create and to edit. |
| impact | string | Default minor.one of: none minor major critical |
| status | string | Status of the update. Default investigating when creating.one of: investigating identified monitoring resolved |
| message | string | Text of the update, at most 5000 characters. Required to create and for updates. |
| notify | boolean | Email the update to the confirmed subscribers of the page (default true). Ignored when editing the title or impact. |
{"title": "Slow API responses in EU","impact": "major"}
response 200 · Incident.
{"id": 1,"page_id": 1,"title": "string","impact": "none","status": "investigating","created_at": 1761868800000,"updated_at": 1761868800000,"resolved_at": 1,"updates": [{"id": 1,"incident_id": 1,"status": "investigating","message": "string","created_at": 1761868800000}]}
/api/status-pages/{id}/incidents/{incident}Delete an incidentwrite token · adminRemoves the incident and all its updates from the page.
| id* | path · integer | Id of the status page. |
| incident* | path · integer | Id of the incident. |
response 200 · Done.
{"ok": true}
/api/status-pages/{id}/incidents/{incident}/updatesPost an incident updatewrite token · adminAdds an update to the timeline; the incident takes its status (resolved closes it, any other status reopens it). With notify (default true) it is emailed to the confirmed subscribers.
| id* | path · integer | Id of the status page. |
| incident* | path · integer | Id of the incident. |
| title | string | One line, at most 200 characters. Required to create and to edit. |
| impact | string | Default minor.one of: none minor major critical |
| status | string | Status of the update. Default investigating when creating.one of: investigating identified monitoring resolved |
| message | string | Text of the update, at most 5000 characters. Required to create and for updates. |
| notify | boolean | Email the update to the confirmed subscribers of the page (default true). Ignored when editing the title or impact. |
{"status": "resolved","message": "Response times are back to normal.","notify": true}
response 201 · Added.
{"incident": {"id": 1,"page_id": 1,"title": "string","impact": "none","status": "investigating","created_at": 1761868800000,"updated_at": 1761868800000,"resolved_at": 1,"updates": [{"id": 1,"incident_id": 1,"status": "investigating","message": "string","created_at": 1761868800000}]},"notified": 1}
/api/status-pages/{id}/incidents/{incident}/updates/{update}Delete an incident updatewrite token · adminRemoves one update; the incident takes the status of the latest one left. The last update cannot be deleted (409): delete the incident instead.
| id* | path · integer | Id of the status page. |
| incident* | path · integer | Id of the incident. |
| update* | path · integer | Id of the update. |
response 200 · Incident.
{"id": 1,"page_id": 1,"title": "string","impact": "none","status": "investigating","created_at": 1761868800000,"updated_at": 1761868800000,"resolved_at": 1,"updates": [{"id": 1,"incident_id": 1,"status": "investigating","message": "string","created_at": 1761868800000}]}
/api/status-pages/{id}/logoUpload a status page logowrite token · adminRaw image file as the body (Content-Type: application/octet-stream), up to 4 MB: SVG, PNG, JPEG, GIF, WebP or AVIF. An SVG is kept as a vector, without scripts, event handlers or external references. Other images are trimmed of transparent borders, scaled down to fit 960×192 pixels and stored as AVIF, WebP and PNG. Replaces the current logo.
| id* | path · integer | Id of the status page. |
response 200 · Stored.
{"logo": {"svg": true,"width": 1,"height": 1,"v": 1}}
/api/status-pages/{id}/logoRemove a status page logowrite token · admin| id* | path · integer | Id of the status page. |
response 200 · Done.
{"ok": true}
/api/status-pages/{id}/logo/urlImport a status page logo from a URLwrite token · adminThe hub downloads the image (public http or https addresses only: private and local networks are refused) and stores it like an upload.
| id* | path · integer | Id of the status page. |
| url* | string |
{"url": "string"}
response 200 · Stored.
{"logo": {"svg": true,"width": 1,"height": 1,"v": 1}}
/api/status-pages/{id}/subscribersList the email subscribers of a status pageadmin sessionAddresses subscribed to incident updates, confirmed or waiting for confirmation (unconfirmed ones are dropped after 7 days). enabled is false when the page has no working email channel.
| id* | path · integer | Id of the status page. |
response 200 · Subscribers.
{"subscribers": [{"id": 1,"page_id": 1,"email": "string","confirmed_at": 1,"created_at": 1761868800000}],"enabled": true}
/api/status-pages/{id}/subscribers/{subscriber}Remove an email subscriberadmin sessionThe address gets no more emails from the page.
| id* | path · integer | Id of the status page. |
| subscriber* | path · integer | Id of the subscriber. |
response 200 · Done.
{"ok": true}
/api/status-pages/previewPreview a status pagewrite token · adminWhat visitors would see with these settings, without saving them. With the id of a saved page, the incidents written on it are included.
| id | integer | |
| slug* | string | URL path: /status/<slug>. 1-50 lowercase letters, digits or dashes. |
| title* | string | |
| description | string | |
| domain | string | Optional custom domain serving the page, e.g. status.example.com. |
| header_url | string | Optional http(s) address the page name links to, e.g. the company website. Empty = the name is not a link. |
| hide_title | boolean | Show only the logo in the page header, without the page name (it stays the browser tab title). Ignored when the page has no logo. |
| logo | PageLogo | The page logo, only in the answer of GET /api/status-pages/{id}. Change it with the logo routes. |
| published | boolean | Drafts are visible only to logged-in users. |
| show_incidents | boolean | |
| monitor_ids | integer[] | |
| group_ids | integer[] | Every monitor of these groups, current and future. |
| created_at | integer | Unix time in milliseconds (0 = never / unknown). |
| subscribe_notifier_id | integer | Email notification channel that sends incident updates to the page's subscribers (its SMTP server and sender are used, its recipients are not). 0 = visitors cannot subscribe. |
| protected | boolean | Visitors need a password to open the page, its badges and its subscription form. Logged-in users open it without one. On save, false removes the password and true keeps the current one. |
| password | string | New password for visitors (6-72 characters), stored hashed and never returned. Needed when protected is set on a page without one. |
{"slug": "acme","title": "Acme status","description": "","domain": "","published": true,"show_incidents": true,"monitor_ids": [7],"group_ids": []}
response 200 · Page.
{"slug": "string","title": "string","description": "string","header_url": "string","logo": {"svg": true,"width": 1,"height": 1,"v": 1},"hide_title": true,"published": true,"status": "operational","days": 1,"monitors": [{"id": 1,"name": "string","group": "string","status": "string","uptime": 12.5,"uptime_24h": 12.5,"bars": [{"t": null,"total": null,"ok": null,"degraded": null,"maint": null,"avg_ms": null}]}],"show_incidents": true,"incidents": [{"id": 1,"monitor_name": "string","started_at": 1761868800000,"resolved_at": 1761868800000}],"updated_at": 1761868800000,"reports": [{"id": 1,"page_id": 1,"title": "string","impact": "none","status": "investigating","created_at": 1761868800000,"updated_at": 1761868800000,"resolved_at": 1,"updates": [null]}],"subscribe": true}
──system
Health, counters, metrics and this document.
/api/docsInteractive API docspublicRedirects to the Swagger UI page of the web app.
/api/healthHealth checkpublicLiveness probe, also returns the hub version.
response 200 · Hub is running.
{"ok": true,"version": "v0.1.0"}
/api/openapi.jsonOpenAPI documentpublicThis document, for the running version.
response 200 · OpenAPI 3.1 document.
{}
/api/overviewDashboard countersread tokenresponse 200 · Counters.
{"monitors": 12,"status": {"up": 10,"down": 1,"paused": 1},"agents": 2,"agents_online": 2,"open_incidents": 1,"version": "v0.1.0"}
/api/telemetryAnonymous usage statsadmin sessionresponse 200 · Usage stats status.
{"enabled": true,"asked": true,"forced_off": true,"report": null,"sent_at": 1,"error": "string"}
/api/telemetryTurn anonymous usage stats on or offadmin session| enabled* | boolean |
{"enabled": true}
response 200 · Usage stats status.
{"enabled": true,"asked": true,"forced_off": true,"report": null,"sent_at": 1,"error": "string"}
/api/versionVersion and update checkread tokenresponse 200 · Version status.
{"current": "string","check_enabled": true,"check_disabled": true,"latest": null,"update_available": true,"checked_at": 1,"error": "string"}
/api/version/checkCheck for a new version nowadmin sessionresponse 200 · Version status.
{"current": "string","check_enabled": true,"check_disabled": true,"latest": null,"update_available": true,"checked_at": 1,"error": "string"}
/metricsPrometheus metricsread tokenMetrics in the Prometheus text format. Public when *settings → general → metrics* is set to public.
response 200 · Metrics.
pingtxt_monitor_up{monitor="Website",type="http"} 1
──auth
Setup, login, password, single sign-on and two-factor. Browser oriented.
/api/auth/2fa/disableDisable two-factorbrowser session| code* | string | TOTP or recovery code. |
{"code": "string"}
response 200 · Done.
{"ok": true}
/api/auth/2fa/enableEnable two-factorbrowser session| code* | string |
{"code": "123456"}
response 200 · Enabled. Recovery codes are shown once.
{"recovery_codes": ["string"]}
/api/auth/2fa/recovery-codesNew recovery codesbrowser sessionReplaces the old codes.
| code* | string |
{"code": "string"}
response 200 · New codes.
{"recovery_codes": ["string"]}
/api/auth/2fa/setupStart two-factor setupbrowser sessionReturns a new TOTP secret; confirm it with /api/auth/2fa/enable.
response 200 · Secret and otpauth:// URI for the QR code.
{"secret": "string","uri": "string"}
/api/auth/identities/{provider}Unlink a single sign-on providerbrowser session| provider* | path · string | one of: github google oidc |
response 200 · Done.
{"ok": true}
/api/auth/loginLog in with passwordpublicSets the pingtxt_session cookie. When the user has two-factor authentication, the response is {"two_factor": true, "challenge": "..."} instead: send the challenge with a code to /api/auth/login/2fa.
| email* | string | |
| password* | string |
{"email": "admin@example.com","password": "correct horse"}
response 200 · Logged in, or a two-factor challenge.
{"id": 1,"email": "string","role": "admin","created_at": 1761868800000,"has_password": true,"two_factor": true,"identities": [{"provider": "string","email": "string","created_at": 1761868800000}]}
/api/auth/login/2faComplete a two-factor loginpublic| challenge* | string | |
| code* | string | 6 digit TOTP code or a recovery code. |
{"challenge": "string","code": "string"}
response 200 · Logged in, session cookie set.
{"id": 1,"email": "string","role": "admin","created_at": 1761868800000,"has_password": true,"two_factor": true,"identities": [{"provider": "string","email": "string","created_at": 1761868800000}]}
/api/auth/logoutLog outpublicEnds the browser session.
response 200 · Done.
{"ok": true}
/api/auth/meCurrent userread tokenresponse 200 · The user behind the session or token.
{"user": {"id": 1,"email": "admin@example.com","role": "admin","created_at": 1761868800000,"has_password": true,"two_factor": false,"identities": []},"version": "v0.1.0","recovery_codes_left": 10}
/api/auth/oauth/{provider}/callbackSingle sign-on callbackpublicRegister <public url>/api/auth/oauth/<provider>/callback at the provider. Not meant to be called directly.
| provider* | path · string | one of: github google oidc |
/api/auth/oauth/{provider}/startStart single sign-onpublicBrowser redirect to the provider. ?next=/path returns there after login, ?link=1 links the provider to the logged-in account.
| provider* | path · string | one of: github google oidc |
| next | query · string | Local path to open after login. |
| link | query · string | 1 to link to the current account. |
/api/auth/optionsLogin optionspublicWhat the login page shows: password form and single sign-on buttons.
response 200 · Options.
{"password_login": true,"providers": [{"id": "github","label": "GitHub"}]}
/api/auth/passwordChange passwordbrowser sessionOther sessions of the user are logged out. Accounts created with single sign-on can set a first password with an empty current.
| current | string | |
| new* | string | At least 8 characters. |
{"current": "string","new": "string"}
response 200 · Done.
{"ok": true}
/api/setupSetup statepublicWhether the instance still needs its first admin account.
response 200 · State.
{"needs_setup": false,"version": "v0.1.0"}
/api/setupCreate the first adminpublicWorks only while no user exists. Starts a session.
| email* | string | |
| password* | string | At least 8 characters. |
{"email": "admin@example.com","password": "correct horse"}
response 201 · Admin created, session cookie set.
{"id": 1,"email": "admin@example.com","role": "admin","created_at": 1761868800000,"has_password": true,"two_factor": false,"identities": []}
──api tokens
Personal tokens and connected OAuth apps. Browser session only.
/api/oauth/grantsList connected appsbrowser sessionApps connected through OAuth (the iOS app, MCP clients): one entry per sign-in.
response 200 · Grants.
[{"id": 1,"client_id": "string","client_name": "string","scope": "string","expires_at": 1761868800000,"created_at": 1761868800000,"last_used_at": 1761868800000,"push_device": "string"}]
/api/oauth/grants/{id}Disconnect an appbrowser session| id* | path · integer | Id of the grant. |
response 200 · Done.
{"ok": true}
/api/tokensList your API tokensbrowser sessionresponse 200 · Tokens of the current user.
[{"id": 1,"name": "string","hint": "string","scope": "read","created_at": 1761868800000,"last_used_at": 1761868800000,"expires_at": 1}]
/api/tokensCreate an API tokenbrowser sessionThe secret is returned once. Read-only users can only create read tokens. At most 50 tokens per user.
| name* | string | Up to 100 characters. |
| scope* | string | one of: read write |
| expires_in_days | integer | 0 = never, up to 3650. |
{"name": "grafana","scope": "read","expires_in_days": 365}
response 201 · Created.
{"token": {"id": 3,"name": "grafana","hint": "x9Qa","scope": "read","created_at": 1761868800000,"last_used_at": 0,"expires_at": 1793404800000},"secret": "ptk_5uG...x9Qa"}
/api/tokens/{id}Revoke an API tokenbrowser session| id* | path · integer | Id of the token. |
response 200 · Done.
{"ok": true}
──users
Accounts and roles. Admin browser session only.
/api/usersList usersadmin sessionresponse 200 · Users.
[{"id": 1,"email": "string","role": "admin","created_at": 1761868800000,"has_password": true,"two_factor": true,"identities": [{"provider": "string","email": "string","created_at": 1761868800000}]}]
/api/usersCreate a useradmin sessionWithout a password the user logs in with single sign-on (matched by verified email).
| email* | string | |
| password | string | Optional, at least 8 characters. |
| role* | string | one of: admin viewer |
{"email": "dev@example.com","role": "viewer"}
response 201 · Created.
{"id": 1,"email": "string","role": "admin","created_at": 1761868800000,"has_password": true,"two_factor": true,"identities": [{"provider": "string","email": "string","created_at": 1761868800000}]}
/api/users/{id}Change a user's roleadmin sessionYou cannot change your own role.
| id* | path · integer | Id of the user. |
| role* | string | one of: admin viewer |
{"role": "admin"}
response 200 · Updated.
{"id": 1,"email": "string","role": "admin","created_at": 1761868800000,"has_password": true,"two_factor": true,"identities": [{"provider": "string","email": "string","created_at": 1761868800000}]}
/api/users/{id}Delete a useradmin session| id* | path · integer | Id of the user. |
response 200 · Done.
{"ok": true}
/api/users/{id}/2faReset a user's two-factoradmin session| id* | path · integer | Id of the user. |
response 200 · Done.
{"ok": true}
──settings
Instance settings. Admin browser session only.
/api/settingsGet instance settingsadmin sessionresponse 200 · Settings.
{"general": {"public_url": "string","local_name": "string","local_enabled": true,"retention_days": 1,"raw_retention_days": 1,"monitor_defaults": {"interval_sec": 1,"timeout_ms": 1,"retries": 1},"expiry": {"cert_days": [1],"domain_days": [1]},"metrics": {"public": true},"degraded_pct": 1,"ip_family": 1,"update_check": {"disabled": true}},"auth": {"password_login": true,"allow_signup": true,"allowed_domains": ["string"],"default_role": "string","providers": [{"id": "string","label": "string","enabled": true,"client_id": "string","client_secret": "string","client_secret_set": true,"callback_url": "string","name": "string","issuer": "string","scopes": "string"}]},"detected_url": "string"}
/api/settings/authSave login settingsadmin session| password_login | boolean | |
| allow_signup | boolean | Create accounts on first single sign-on login. |
| allowed_domains | string[] | Email domains allowed to sign up, empty = any. |
| default_role | string | one of: admin viewer |
| providers | object | Keyed by github, google, oidc. |
{"password_login": true,"allow_signup": true,"allowed_domains": ["string"],"default_role": "admin","providers": {"key": {"enabled": true,"client_id": "string","client_secret": "string","name": "string","issuer": "string","scopes": "string"}}}
response 200 · Settings.
{"general": {"public_url": "string","local_name": "string","local_enabled": true,"retention_days": 1,"raw_retention_days": 1,"monitor_defaults": {"interval_sec": 1,"timeout_ms": 1,"retries": 1},"expiry": {"cert_days": [1],"domain_days": [1]},"metrics": {"public": true},"degraded_pct": 1,"ip_family": 1,"update_check": {"disabled": true}},"auth": {"password_login": true,"allow_signup": true,"allowed_domains": ["string"],"default_role": "string","providers": [{"id": "string","label": "string","enabled": true,"client_id": "string","client_secret": "string","client_secret_set": true,"callback_url": "string","name": "string","issuer": "string","scopes": "string"}]},"detected_url": "string"}
/api/settings/generalSave general settingsadmin session| public_url | string | Public URL of the hub, used in links, agent setup and OAuth callbacks. |
| local_name | string | Name of the hub's own location. |
| local_enabled | boolean | The hub runs checks itself. |
| retention_days | integer | Hourly history, up to 3650. |
| raw_retention_days | integer | Every single check. |
| monitor_defaults | object | |
| expiry | object | |
| metrics | object | |
| degraded_pct | integer | Default degraded threshold, 1 to 50. |
| ip_family | integer | Default IP family of monitors with ip_family 0: 1 IPv4 or IPv6 (default), 4 IPv4 only, 6 IPv6 only, 46 IPv4 and IPv6. Locations without IPv6 check those monitors over IPv4.one of: 1 4 6 46 |
| update_check | object |
{"public_url": "string","local_name": "string","local_enabled": true,"retention_days": 1,"raw_retention_days": 1,"monitor_defaults": {"interval_sec": 1,"timeout_ms": 1,"retries": 1},"expiry": {"cert_days": [1],"domain_days": [1]},"metrics": {"public": true},"degraded_pct": 1,"ip_family": 1,"update_check": {"disabled": true}}
response 200 · Settings.
{"general": {"public_url": "string","local_name": "string","local_enabled": true,"retention_days": 1,"raw_retention_days": 1,"monitor_defaults": {"interval_sec": 1,"timeout_ms": 1,"retries": 1},"expiry": {"cert_days": [1],"domain_days": [1]},"metrics": {"public": true},"degraded_pct": 1,"ip_family": 1,"update_check": {"disabled": true}},"auth": {"password_login": true,"allow_signup": true,"allowed_domains": ["string"],"default_role": "string","providers": [{"id": "string","label": "string","enabled": true,"client_id": "string","client_secret": "string","client_secret_set": true,"callback_url": "string","name": "string","issuer": "string","scopes": "string"}]},"detected_url": "string"}
/api/settings/pushGet mobile app push settingsadmin sessionresponse 200 · Push settings.
{"enabled": true,"relay_url": "string","hub_id": "string","registered": true,"devices": 1}
/api/settings/pushSave mobile app push settingsadmin sessionTurning push on registers the hub with the relay the first time, and again when the relay no longer accepts the hub token (its secret changed): the phones then enroll again. A different relay URL drops the registration and every phone (they enroll again).
| enabled | boolean | |
| relay_url | string |
{"enabled": true,"relay_url": "https://push.pingtxt.com"}
response 200 · Saved.
{"enabled": true,"relay_url": "string","hub_id": "string","registered": true,"devices": 1}
/api/settings/push/testTest the push relayadmin sessionChecks the hub token at the relay and that the relay can reach Firebase.
response 200 · The relay accepts this hub.
{"ok": true,"hub_id": "string"}
──backups
Database backups and configuration export/import.
/api/backupsList backupsadmin sessionresponse 200 · Backups and settings.
{"settings": {"enabled": true,"interval_hours": 1,"keep": 1},"backups": [{"name": "string","kind": "auto","size": 1,"created_at": 1761868800000}],"dir": "string","pending_restore": {"name": "string","staged_at": 1761868800000},"driver": "string","file_backups": true}
/api/backupsBack up nowadmin sessionresponse 201 · Backup file.
{"name": "string","kind": "auto","size": 1,"created_at": 1761868800000}
/api/backups/{name}Download a backupadmin session| name* | path · string | Backup file name. |
/api/backups/{name}Delete a backupadmin session| name* | path · string | Backup file name. |
response 200 · Done.
{"ok": true}
/api/backups/{name}/restoreStage a restoreadmin sessionThe backup replaces the database at the next start of the hub.
| name* | path · string | Backup file name. |
response 200 · Staged.
{"pending_restore": {"name": "string","staged_at": 1761868800000},"info": {"schema_version": 1,"monitors": 1,"users": 1}}
/api/backups/restoreCancel a staged restoreadmin sessionresponse 200 · Done.
{"ok": true}
/api/backups/uploadUpload a backupadmin sessionRaw SQLite file as the body (Content-Type: application/octet-stream), up to 4 GB. SQLite only.
response 201 · Stored.
{"backup": {"name": "string","kind": "auto","size": 1,"created_at": 1761868800000},"info": {"schema_version": 1,"monitors": 1,"users": 1}}
/api/config/exportExport the configurationread tokenAdmins only. Without secrets by default; ?secrets=1 needs a browser session (API tokens get 403).
| secrets | query · string | 1 to include passwords and tokens. |
response 200 · JSON download.
{"format": "string","version": 1,"hub_version": "string","exported_at": "string","secrets": true,"settings": {},"agents": [{}],"groups": [{}],"tags": [{}],"notifiers": [{}],"monitors": [{}],"maintenances": [{}],"status_pages": [{}]}
/api/config/importImport a configurationadmin sessionA backup is taken first. Body up to 64 MB.
| mode | query · string | replace deletes what is not in the document.one of: merge replace |
| settings | query · string | 0 to leave instance settings alone. |
| format | string | Always "pingtxt-config". |
| version | integer | |
| hub_version | string | |
| exported_at | string | |
| secrets | boolean | Passwords and tokens are included. |
| settings | object | |
| agents | object[] | |
| groups | object[] | |
| tags | object[] | |
| notifiers | object[] | |
| monitors | object[] | |
| maintenances | object[] | |
| status_pages | object[] |
{"format": "string","version": 1,"hub_version": "string","exported_at": "string","secrets": true,"settings": {},"agents": [{}],"groups": [{}],"tags": [{}],"notifiers": [{}],"monitors": [{}],"maintenances": [{}],"status_pages": [{}]}
response 200 · Result.
{"mode": "merge","backup": "string","created": {"key": 1},"updated": {"key": 1},"deleted": {"key": 1},"skipped": {"key": 1},"settings": true,"warnings": ["string"]}
/api/import/uptime-kumaImport from Uptime Kumaadmin sessionCreates monitors from an Uptime Kuma 1.x backup file (Settings → Backup → Export in Uptime Kuma). Kuma groups become groups, tags become tags, paused monitors stay paused. Monitors whose name already exists are left alone; unsupported types are skipped with a reason. Notification channels are not imported. With dry_run=1 nothing is created and the answer is the preview.
| dry_run | query · boolean | Preview only. |
| version | string | |
| monitorList* | object[] |
{"version": "string","monitorList": [{}]}
response 200 · What was (or would be) done with each monitor.
{"version": "string","monitors": [{"name": "string","kuma_type": "string","type": "string","target": "string","group": "string","paused": true,"action": "create","reason": "string","note": "string"}],"groups": ["string"],"tags": ["string"],"warnings": ["string"],"created": 1}
/api/settings/backupsSave backup scheduleadmin session| enabled | boolean | |
| interval_hours | integer | |
| keep | integer | Scheduled backups to keep. |
{"enabled": true,"interval_hours": 24,"keep": 7}
response 200 · Saved.
{"enabled": true,"interval_hours": 1,"keep": 1}
──oauth
OAuth 2.1 authorization server for native apps and MCP clients. Form bodies, OAuth error format.
/.well-known/oauth-authorization-serverAuthorization server metadatapublicRFC 8414 metadata: endpoints, scopes, PKCE methods.
response 200 · Metadata.
{}
/oauth/authorizeStart a sign-inpublicOpen in a browser (ASWebAuthenticationSession on iOS). The user logs in on the hub if needed and allows the app on a consent page; the browser then goes to redirect_uri with code, state and iss, or with error.
| response_type* | query · string | code. |
| client_id* | query · string | pingtxt-ios for the iOS app. |
| redirect_uri* | query · string | pingtxt://oauth for the iOS app. |
| code_challenge* | query · string | BASE64URL(SHA256(code_verifier)), no padding. |
| code_challenge_method* | query · string | S256. |
| state* | query · string | Random value, checked on return. |
| scope | query · string | read, write or read write: write is preselected on the consent page for admins. |
| resource | query · string | Optional (RFC 8707): <hub>/api or <hub>/mcp. |
/oauth/revokeSign outpublicEnds the sign-in of the token (access or refresh token, RFC 7009). Always answers 200.
| token* | string | Refresh or access token. |
| client_id* | string | pingtxt-ios for the iOS app. |
/oauth/tokenGet or refresh tokenspublicgrant_type=authorization_code exchanges the code (single use, 5 minutes) for tokens. grant_type=refresh_token returns a new access token and a new refresh token: the old one stops working, and using it again revokes the whole sign-in, so refresh one request at a time and store the new refresh token before using it.
| grant_type* | string | authorization_code or refresh_token. |
| client_id* | string | pingtxt-ios for the iOS app. |
| code | string | authorization_code: the code from the redirect. |
| redirect_uri | string | authorization_code: same as in the authorization request. |
| code_verifier | string | authorization_code: the PKCE verifier. |
| refresh_token | string | refresh_token: the latest refresh token. |
response 200 · Tokens.
{"access_token": "string","token_type": "Bearer","expires_in": 1,"refresh_token": "string","scope": "read"}