$ curl /api/openapi.json

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/monitors
curl -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.

get/api/monitorsList monitorsread token

Every monitor with its last 24 hours. Secrets (passwords in targets, headers, bodies, push tokens) are hidden from read-only users.

parameters
groupquery · stringGroup id, or none for monitors without a group.
tagquery · integerTag 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
}
}
]
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired
post/api/monitorsCreate a monitorwrite token · admin
request body · application/json
name*string
type*stringone of: http keyword tcp ping dns push postgres mysql redis mongodb docker grpc smtp mcp
targetstringWhat 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_secintegerSeconds between checks, 10 to 86400 (push: expected period, up to 7 days). 0 = default (60, push 3600).
timeout_msinteger500 to 60000. 0 = 10000.
retriesintegerConsecutive failures before a location counts as down, 1 to 10.
methodstringHTTP method for http/keyword (default GET). For mcp: the transport, auto, streamable or sse.
expected_statusstringAccepted HTTP status codes, e.g. "200-299" or "200,301". For mcp: the minimum number of tools.
keywordstringkeyword: text that must appear in the body. mcp: comma separated tool names that must be listed.
keyword_invertbooleankeyword: fail when the text is found.
dns_recordstringdns: record type (A, AAAA, CNAME, MX, TXT, NS...).
dns_serverstringdns: resolver to query, host[:port]. Empty = system resolver.
dns_expectedstringdns: value that must be in the answer.
ignore_tlsbooleanAccept invalid TLS certificates.
ip_familyinteger0 = 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
headersobjectRequest headers (http, keyword, mcp).
bodystringRequest body (http, keyword).
assertionsAssertion[]JSON response rules, http and keyword only, at most 20.
grace_secintegerpush: extra time after the period before the monitor goes down, 0 to 86400.
degraded_pctintegerShare of failing locations (percent) from which the monitor is degraded, 1 to 50. 0 = instance default.
locationsstring[]Where the check runs: "local" (the hub) and/or "agent:<id>". Empty = local. See GET /api/locations.
group_idintegerGroup, 0 = none.
tag_idsinteger[]
notifiersChannelLink[]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_alertbooleanAlert before the TLS certificate expires (https targets).
domain_expiry_alertbooleanAlert before the domain registration expires.
pausedbooleanCreate the monitor paused. Ignored on update: use POST /api/monitors/{id}/pause.
example
{
"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
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
get/api/monitors/{id}Get a monitorread token

Includes 7 and 30 day uptime and the last result of each location.

parameters
id*path · integerId 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"
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 404 not found
put/api/monitors/{id}Update a monitorwrite token · admin

Replaces the monitor: send every field.

parameters
id*path · integerId of the monitor.
request body · application/json
name*string
type*stringone of: http keyword tcp ping dns push postgres mysql redis mongodb docker grpc smtp mcp
targetstringWhat 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_secintegerSeconds between checks, 10 to 86400 (push: expected period, up to 7 days). 0 = default (60, push 3600).
timeout_msinteger500 to 60000. 0 = 10000.
retriesintegerConsecutive failures before a location counts as down, 1 to 10.
methodstringHTTP method for http/keyword (default GET). For mcp: the transport, auto, streamable or sse.
expected_statusstringAccepted HTTP status codes, e.g. "200-299" or "200,301". For mcp: the minimum number of tools.
keywordstringkeyword: text that must appear in the body. mcp: comma separated tool names that must be listed.
keyword_invertbooleankeyword: fail when the text is found.
dns_recordstringdns: record type (A, AAAA, CNAME, MX, TXT, NS...).
dns_serverstringdns: resolver to query, host[:port]. Empty = system resolver.
dns_expectedstringdns: value that must be in the answer.
ignore_tlsbooleanAccept invalid TLS certificates.
ip_familyinteger0 = 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
headersobjectRequest headers (http, keyword, mcp).
bodystringRequest body (http, keyword).
assertionsAssertion[]JSON response rules, http and keyword only, at most 20.
grace_secintegerpush: extra time after the period before the monitor goes down, 0 to 86400.
degraded_pctintegerShare of failing locations (percent) from which the monitor is degraded, 1 to 50. 0 = instance default.
locationsstring[]Where the check runs: "local" (the hub) and/or "agent:<id>". Empty = local. See GET /api/locations.
group_idintegerGroup, 0 = none.
tag_idsinteger[]
notifiersChannelLink[]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_alertbooleanAlert before the TLS certificate expires (https targets).
domain_expiry_alertbooleanAlert before the domain registration expires.
pausedbooleanCreate the monitor paused. Ignored on update: use POST /api/monitors/{id}/pause.
example
{
"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
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
delete/api/monitors/{id}Delete a monitorwrite token · admin

Also deletes its history.

parameters
id*path · integerId of the monitor.
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
post/api/monitors/{id}/expiryCheck expiry dates nowwrite token · admin

Looks up the TLS certificate and domain expiry.

parameters
id*path · integerId 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
}
]
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
post/api/monitors/{id}/pausePause or resumewrite token · admin

Pausing resolves the open incident.

parameters
id*path · integerId of the monitor.
request body · application/json
paused*boolean
example
{
"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
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
post/api/monitors/{id}/push-tokenRotate the heartbeat URLwrite token · admin

push monitors only: the old URL stops working.

parameters
id*path · integerId 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
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
get/api/monitors/{id}/resultsLatest check resultsread token
parameters
id*path · integerId of the monitor.
limitquery · integerNumber of results.
beforequery · integerOnly results older than this time (unix ms), to load the next page.
fromquery · integerOnly results at or after this time (unix ms).
toquery · integerOnly results before this time (unix ms).
failedquery · booleanOnly 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"
}
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired
get/api/monitors/{id}/seriesResponse time seriesread token

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

parameters
id*path · integerId of the monitor.
rangequery · stringTime range.one of: 1h 6h 24h 7d 30d
startquery · integerCustom window start, unix ms. With end, replaces range; 5 minutes to 400 days long.
endquery · integerCustom 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
}
}
}
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired
post/api/monitors/{id}/testCheck nowwrite token · admin

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

parameters
id*path · integerId 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"
}
]
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
post/api/monitors/bulkChange several monitorswrite token · admin

Pauses, 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.

request body · application/json
ids*integer[]
action*stringone of: pause resume delete group
group_idintegerAction group only: the group, 0 for none.
example
{
"ids": [
1,
2,
3
],
"action": "pause"
}
response 200 · Number of monitors selected.
{
"count": 3
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
post/api/monitors/testTest an unsaved monitorwrite token · admin

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

request body · application/json
name*string
type*stringone of: http keyword tcp ping dns push postgres mysql redis mongodb docker grpc smtp mcp
targetstringWhat 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_secintegerSeconds between checks, 10 to 86400 (push: expected period, up to 7 days). 0 = default (60, push 3600).
timeout_msinteger500 to 60000. 0 = 10000.
retriesintegerConsecutive failures before a location counts as down, 1 to 10.
methodstringHTTP method for http/keyword (default GET). For mcp: the transport, auto, streamable or sse.
expected_statusstringAccepted HTTP status codes, e.g. "200-299" or "200,301". For mcp: the minimum number of tools.
keywordstringkeyword: text that must appear in the body. mcp: comma separated tool names that must be listed.
keyword_invertbooleankeyword: fail when the text is found.
dns_recordstringdns: record type (A, AAAA, CNAME, MX, TXT, NS...).
dns_serverstringdns: resolver to query, host[:port]. Empty = system resolver.
dns_expectedstringdns: value that must be in the answer.
ignore_tlsbooleanAccept invalid TLS certificates.
ip_familyinteger0 = 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
headersobjectRequest headers (http, keyword, mcp).
bodystringRequest body (http, keyword).
assertionsAssertion[]JSON response rules, http and keyword only, at most 20.
grace_secintegerpush: extra time after the period before the monitor goes down, 0 to 86400.
degraded_pctintegerShare of failing locations (percent) from which the monitor is degraded, 1 to 50. 0 = instance default.
locationsstring[]Where the check runs: "local" (the hub) and/or "agent:<id>". Empty = local. See GET /api/locations.
group_idintegerGroup, 0 = none.
tag_idsinteger[]
notifiersChannelLink[]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_alertbooleanAlert before the TLS certificate expires (https targets).
domain_expiry_alertbooleanAlert before the domain registration expires.
pausedbooleanCreate the monitor paused. Ignored on update: use POST /api/monitors/{id}/pause.
example
{
"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"
}
]
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
get/api/push/{token}Send a heartbeatpublic

Heartbeat 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

parameters
token*path · string
statusquery · stringResult of the job.one of: up down ok fail 0 1
msgquery · stringMessage shown in the history, up to 200 characters.
pingquery · numberDuration or latency in ms.
response 200 · Recorded.
{
"ok": true
}
errors: 400 invalid input · 404 not found
post/api/push/{token}Send a heartbeatpublic

Heartbeat 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

parameters
token*path · string
statusquery · stringResult of the job.one of: up down ok fail 0 1
msgquery · stringMessage shown in the history, up to 200 characters.
pingquery · numberDuration or latency in ms.
response 200 · Recorded.
{
"ok": true
}
errors: 400 invalid input · 404 not found

──incidents

Down periods of monitors.

get/api/incidentsList incidentsread token

The latest 100, open ones first.

parameters
monitorquery · integerOnly this monitor.
statequery · stringOnly open or only resolved incidents.one of: open resolved
sincequery · integerOnly incidents open at some point after this time (unix ms).
limitquery · integerMaximum number of incidents.
response 200 · Incidents.
[
{
"id": 1,
"monitor_id": 1,
"monitor_name": "string",
"started_at": 1761868800000,
"resolved_at": 1,
"cause": "string"
}
]
errors: 401 not logged in, or the token is invalid or expired

──maintenance

Windows during which monitors don't alert.

get/api/maintenancesList maintenance windowsread token
response 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
}
]
errors: 401 not logged in, or the token is invalid or expired
post/api/maintenancesCreate a maintenance windowwrite token · admin

Covered monitors don't alert and don't count as down.

request body · application/json
name*string
descriptionstring
all_monitorsbooleanCovers every monitor.
monitor_idsinteger[]
group_idsinteger[]Every monitor of these groups.
tag_idsinteger[]Every monitor with one of these tags.
kind*stringone of: once daily weekly monthly
start_atintegeronce: start. Recurring: first day the schedule is valid (0 = now).
end_atintegeronce: end. Recurring: last day the schedule is valid (0 = forever).
start_timestringRecurring: local start time, HH:MM.
duration_minintegerRecurring: length in minutes, up to 7 days.
weekdaysinteger[]weekly: 0 = Sunday … 6 = Saturday.
month_daysinteger[]monthly: 1 … 31.
timezonestringIANA time zone, default UTC.
enabledboolean
example
{
"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
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
put/api/maintenances/{id}Update a maintenance windowwrite token · admin
parameters
id*path · integerId of the maintenance window.
request body · application/json
name*string
descriptionstring
all_monitorsbooleanCovers every monitor.
monitor_idsinteger[]
group_idsinteger[]Every monitor of these groups.
tag_idsinteger[]Every monitor with one of these tags.
kind*stringone of: once daily weekly monthly
start_atintegeronce: start. Recurring: first day the schedule is valid (0 = now).
end_atintegeronce: end. Recurring: last day the schedule is valid (0 = forever).
start_timestringRecurring: local start time, HH:MM.
duration_minintegerRecurring: length in minutes, up to 7 days.
weekdaysinteger[]weekly: 0 = Sunday … 6 = Saturday.
month_daysinteger[]monthly: 1 … 31.
timezonestringIANA time zone, default UTC.
enabledboolean
example
{
"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
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
delete/api/maintenances/{id}Delete a maintenance windowwrite token · admin
parameters
id*path · integerId of the maintenance window.
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication

──groups and tags

Organize monitors; status pages and maintenance windows can select them by group or tag.

get/api/groupsList groupsread token
response 200 · Groups.
[
{
"id": 1,
"name": "string",
"description": "string",
"monitors": 1,
"created_at": 1761868800000
}
]
errors: 401 not logged in, or the token is invalid or expired
post/api/groupsCreate a groupwrite token · admin
request body · application/json
idinteger
name*stringUnique, up to 64 characters.
descriptionstringUp to 500 characters.
monitorsintegerNumber of monitors in the group (read only).
created_atintegerUnix time in milliseconds (0 = never / unknown).
example
{
"name": "Production",
"description": "Customer facing"
}
response 201 · Created.
{
"id": 1,
"name": "string",
"description": "string",
"monitors": 1,
"created_at": 1761868800000
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 409 already exists
put/api/groups/{id}Update a groupwrite token · admin
parameters
id*path · integerId of the group.
request body · application/json
idinteger
name*stringUnique, up to 64 characters.
descriptionstringUp to 500 characters.
monitorsintegerNumber of monitors in the group (read only).
created_atintegerUnix time in milliseconds (0 = never / unknown).
example
{
"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
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found · 409 already exists
delete/api/groups/{id}Delete a groupwrite token · admin

Its monitors are kept, without a group.

parameters
id*path · integerId of the group.
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
put/api/groups/{id}/monitorsSet the monitors of a groupwrite token · admin

Monitors listed here move to this group; monitors of the group not listed leave it.

parameters
id*path · integerId of the group.
request body · application/json
monitor_ids*integer[]
example
{
"monitor_ids": [
7,
8
]
}
response 200 · Group.
{
"id": 1,
"name": "string",
"description": "string",
"monitors": 1,
"created_at": 1761868800000
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
get/api/tagsList tagsread token
response 200 · Tags.
[
{
"id": 1,
"name": "string",
"color": "red",
"monitors": 1,
"created_at": 1761868800000
}
]
errors: 401 not logged in, or the token is invalid or expired
post/api/tagsCreate a tagwrite token · admin
request body · application/json
idinteger
name*stringUnique, up to 32 characters, no commas.
colorstringone of: "" red orange yellow green cyan blue purple pink gray
monitorsintegerNumber of tagged monitors (read only).
created_atintegerUnix time in milliseconds (0 = never / unknown).
example
{
"name": "critical",
"color": "red"
}
response 201 · Created.
{
"id": 1,
"name": "string",
"color": "red",
"monitors": 1,
"created_at": 1761868800000
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 409 already exists
put/api/tags/{id}Update a tagwrite token · admin
parameters
id*path · integerId of the tag.
request body · application/json
idinteger
name*stringUnique, up to 32 characters, no commas.
colorstringone of: "" red orange yellow green cyan blue purple pink gray
monitorsintegerNumber of tagged monitors (read only).
created_atintegerUnix time in milliseconds (0 = never / unknown).
example
{
"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
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found · 409 already exists
delete/api/tags/{id}Delete a tagwrite token · admin
parameters
id*path · integerId of the tag.
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication

──agents

Remote check locations connected to the hub.

get/api/agentsList agentsread token
response 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
}
errors: 401 not logged in, or the token is invalid or expired
post/api/agentsCreate an agentwrite token · admin

The agent token is returned once.

request body · application/json
name*string
locationstring
example
{
"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"
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
put/api/agents/{id}Rename an agentwrite token · admin
parameters
id*path · integerId of the agent.
request body · application/json
name*string
locationstring
example
{
"name": "string",
"location": "string"
}
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
delete/api/agents/{id}Delete an agentwrite token · admin

Disconnects it and removes it from monitor locations.

parameters
id*path · integerId of the agent.
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
get/api/agents/{id}/monitorsPreview compatible monitorsread token

Monitors this agent can be added to, and the ones skipped with the reason. Heartbeat monitors have no locations and are left out.

parameters
id*path · integerId 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
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 404 not found
post/api/agents/{id}/monitorsAdd the agent to monitorswrite token · admin

Adds the agent's location to the given monitors. Monitors that are not compatible are skipped and returned with the reason.

parameters
id*path · integerId of the agent.
request body · application/json
monitor_ids*integer[]
example
{
"monitor_ids": [
1,
4,
7
]
}
response 200 · Done.
{
"added": 1,
"skipped": [
{
"id": 1,
"name": "string",
"type": "string",
"target": "string",
"reason": "no_ipv6"
}
]
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
post/api/agents/{id}/tokenRotate the agent tokenwrite token · admin

The agent is disconnected until it uses the new token.

parameters
id*path · integerId of the agent.
response 200 · New token.
{
"token": "string",
"hub_url": "string"
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
post/api/agents/{id}/updateUpdate an agent nowwrite token · admin

Asks a connected agent to install the hub's version.

parameters
id*path · integerId of the agent.
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
get/api/locationsList check locationsread token

The 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
}
]
errors: 401 not logged in, or the token is invalid or expired
put/api/settings/agent-updatesAgent auto-update settingwrite token · admin
request body · application/json
auto_update*boolean
example
{
"auto_update": true
}
response 200 · Saved.
{
"auto_update": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication

──notifications

Alert channels: webhook, Discord, Slack, Telegram, ntfy, email.

get/api/mobile/configPush config for the mobile appread token

Where 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
}
errors: 401 not logged in, or the token is invalid or expired
get/api/mobile/devicesList my phonesread token
response 200 · Phones of the current user.
[
{
"id": 1,
"name": "string",
"created_at": 1761868800000,
"last_seen_at": 1761868800000
}
]
errors: 401 not logged in, or the token is invalid or expired
post/api/mobile/devicesRegister my phoneread token

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

request body · application/json
device*string
namestring
example
{
"device": "prd_AeYWsf5y...",
"name": "Federico's iPhone"
}
response 200 · Saved.
{
"id": 3
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 409 already exists
delete/api/mobile/devices/{id}Remove my phoneread token

The phone stops getting notifications from this hub.

parameters
id*path · integerId of the phone.
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 404 not found
put/api/mobile/live-activity/start-tokenSet my Live Activity push-to-start tokenread token

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

request body · application/json
token*string
hub_url*string
example
{
"token": "80f4c3...",
"hub_url": "https://status.example.com"
}
response 200 · Saved.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 409 already exists
delete/api/mobile/live-activity/start-tokenRemove my Live Activity push-to-start tokenread token

The hub no longer starts the activity on this phone (for example, Live Activities turned off in the app).

response 200 · Saved.
{
"ok": true
}
errors: 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 409 already exists
put/api/mobile/live-activity/tokenSet the token of my running Live Activityread token

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

request body · application/json
token*string
example
{
"token": "6a0d1e..."
}
response 200 · Saved.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 409 already exists
delete/api/mobile/live-activity/tokenRemove the token of my Live Activityread token

Call it when the activity ends on the phone (ended by the app or dismissed by the user).

response 200 · Saved.
{
"ok": true
}
errors: 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 409 already exists
get/api/notifiersList notification channelsread token
response 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
}
]
errors: 401 not logged in, or the token is invalid or expired
post/api/notifiersCreate a notification channelwrite token · admin
request body · application/json
idinteger
name*string
type*stringone of: webhook discord slack telegram ntfy email app
config*objectwebhook, 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 ********.
enabledboolean
delay_minintegerMinutes 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_minintegerRepeat 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_monitorsbooleanThe channel covers every monitor, also the ones added later. Default true when left out on create.
group_idsinteger[]Without all_monitors: the channel covers the monitors of these groups.
tag_idsinteger[]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.
monitorsChannelLink[]Monitors linked one by one (monitor_id + events), the same links as the monitors' notifiers. Left out on update: kept; [] removes them.
created_atintegerUnix time in milliseconds (0 = never / unknown).
example
{
"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
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 409 already exists
put/api/notifiers/{id}Update a notification channelwrite token · admin
parameters
id*path · integerId of the channel.
request body · application/json
idinteger
name*string
type*stringone of: webhook discord slack telegram ntfy email app
config*objectwebhook, 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 ********.
enabledboolean
delay_minintegerMinutes 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_minintegerRepeat 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_monitorsbooleanThe channel covers every monitor, also the ones added later. Default true when left out on create.
group_idsinteger[]Without all_monitors: the channel covers the monitors of these groups.
tag_idsinteger[]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.
monitorsChannelLink[]Monitors linked one by one (monitor_id + events), the same links as the monitors' notifiers. Left out on update: kept; [] removes them.
created_atintegerUnix time in milliseconds (0 = never / unknown).
example
{
"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
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 409 already exists
delete/api/notifiers/{id}Delete a notification channelwrite token · admin
parameters
id*path · integerId of the channel.
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
post/api/notifiers/{id}/testTest a channelwrite token · admin
parameters
id*path · integerId of the channel.
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found · 409 already exists · 502 the notification could not be delivered
post/api/notifiers/testTest an unsaved channelwrite token · admin

Sends a test alert with this configuration. With an id, empty secrets are taken from that saved channel.

request body · application/json
idinteger
name*string
type*stringone of: webhook discord slack telegram ntfy email app
config*objectwebhook, 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 ********.
enabledboolean
delay_minintegerMinutes 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_minintegerRepeat 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_monitorsbooleanThe channel covers every monitor, also the ones added later. Default true when left out on create.
group_idsinteger[]Without all_monitors: the channel covers the monitors of these groups.
tag_idsinteger[]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.
monitorsChannelLink[]Monitors linked one by one (monitor_id + events), the same links as the monitors' notifiers. Left out on update: kept; [] removes them.
created_atintegerUnix time in milliseconds (0 = never / unknown).
example
{
"name": "ops",
"type": "slack",
"config": {
"url": "https://hooks.slack.com/services/T000/B000/XXXX"
},
"enabled": true
}
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 409 already exists · 502 the notification could not be delivered

──reports

Periodic email reports: uptime and incidents of the last week or month, per monitor group.

get/api/reportsGet report settingsread token

Report 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
}
}
}
errors: 401 not logged in, or the token is invalid or expired
put/api/reportsSave report settingswrite token · admin

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

request body · application/json
weeklybooleanSend a report every Monday for the week before.
monthlybooleanSend a report on the 1st for the month before.
hourintegerHour of the day the reports are sent, in timezone.
timezonestringIANA time zone of the periods and of the times in the report.
notifier_idsinteger[]Email channels that receive the reports. Disabled channels are skipped.
group_idsinteger[]Only these monitor groups. Empty: every monitor.
example
{
"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
}
}
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
get/api/reports/previewPreview a reportread token

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

parameters
kindquery · stringWhich 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"
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired
post/api/reports/sendSend a report nowwrite token · admin

Emails the report of the last complete period to the saved channels now. The schedule does not change.

request body · application/json
kindstringone of: weekly monthly
example
{
"kind": "monthly"
}
response 200 · Sent to at least one channel.
{
"ok": true,
"sent": 1
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 502 the notification could not be delivered

──status pages

Public pages and their data.

get/api/badge/{slug}/{kind}Status page badgepublic

SVG badge with the overall state of a status page (operational, degraded, partial outage, major outage), to embed in a README: ``markdown ![status](https://pingtxt.example.com/api/badge/acme/status.svg)

parameters
slug*path · stringSlug of a published status page.
kind*path · stringstatus, optionally with .svg.one of: status status.svg
labelquery · stringReplaces the left text (up to 60 characters).
stylequery · stringflat (default) or flat-square.one of: flat flat-square
response 200 · SVG badge.
errors: 404 not found badge
get/api/badge/{slug}/{monitor}/{kind}Monitor badgepublic

SVG 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 ![uptime](https://pingtxt.example.com/api/badge/acme/12/uptime.svg?period=7d)

parameters
slug*path · stringSlug of a published status page.
monitor*path · integerMonitor id.
kind*path · stringstatus, uptime or response, optionally with .svg.one of: status uptime response status.svg uptime.svg response.svg
periodquery · stringWindow for uptime (default 30d) and response (default 24h).one of: 24h 7d 30d 90d
labelquery · stringReplaces the left text (up to 60 characters).
stylequery · stringflat (default) or flat-square.one of: flat flat-square
response 200 · SVG badge.
errors: 400 bad period badge · 404 not found badge
get/api/public/status/{slug}Public status page datapublic

What /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).

parameters
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
}
errors: 403 the page needs a password: unlock it with `post /api/public/status/{slug}/unlock` · 404 not found
post/api/public/status/{slug}/subscribeSubscribe to a status page by emailpublic

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

parameters
slug*path · string
request body · application/json
email*string
example
{
"email": "jane@example.com"
}
response 202 · Confirmation email queued (or already subscribed).
{
"ok": true
}
errors: 400 invalid input · 404 not found · 409 already exists · 429 too many failed attempts. password logins are limited to 10 failures per account and 30 per client address in 15 minutes; second factor codes to 10 wrong codes per user. wait for `retry-after` seconds
post/api/public/status/{slug}/unlockUnlock a password protected status pagepublic

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

parameters
slug*path · string
request body · application/json
password*string
example
{
"password": "string"
}
response 200 · Unlocked (or the page has no password); the cookie is set.
{
"ok": true
}
errors: 400 invalid input · 403 wrong password · 404 not found · 429 too many failed attempts. password logins are limited to 10 failures per account and 30 per client address in 15 minutes; second factor codes to 10 wrong codes per user. wait for `retry-after` seconds
post/api/public/subscriptions/{token}/confirmConfirm an email subscriptionpublic

Called by the status page when a visitor opens the link of the confirmation email. Links expire after 7 days.

parameters
token*path · stringToken of the links sent by email.
response 200 · Confirmed.
{
"ok": true,
"email": "string",
"page": "string"
}
errors: 404 not found
post/api/public/subscriptions/{token}/unsubscribeUnsubscribe from a status pagepublic

Removes 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).

parameters
token*path · stringToken of the links sent by email.
response 200 · Unsubscribed.
{
"ok": true,
"email": "string",
"page": "string"
}
errors: 404 not found
get/api/status-pagesList status pagesread token
response 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"
}
errors: 401 not logged in, or the token is invalid or expired
post/api/status-pagesCreate a status pagewrite token · admin
request body · application/json
idinteger
slug*stringURL path: /status/<slug>. 1-50 lowercase letters, digits or dashes.
title*string
descriptionstring
domainstringOptional custom domain serving the page, e.g. status.example.com.
header_urlstringOptional http(s) address the page name links to, e.g. the company website. Empty = the name is not a link.
hide_titlebooleanShow only the logo in the page header, without the page name (it stays the browser tab title). Ignored when the page has no logo.
logoPageLogoThe page logo, only in the answer of GET /api/status-pages/{id}. Change it with the logo routes.
publishedbooleanDrafts are visible only to logged-in users.
show_incidentsboolean
monitor_idsinteger[]
group_idsinteger[]Every monitor of these groups, current and future.
created_atintegerUnix time in milliseconds (0 = never / unknown).
subscribe_notifier_idintegerEmail 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.
protectedbooleanVisitors 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.
passwordstringNew password for visitors (6-72 characters), stored hashed and never returned. Needed when protected is set on a page without one.
example
{
"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"
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 409 already exists
get/api/status-pages/{id}Get a status pageread token
parameters
id*path · integerId 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"
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 404 not found
put/api/status-pages/{id}Update a status pagewrite token · admin
parameters
id*path · integerId of the status page.
request body · application/json
idinteger
slug*stringURL path: /status/<slug>. 1-50 lowercase letters, digits or dashes.
title*string
descriptionstring
domainstringOptional custom domain serving the page, e.g. status.example.com.
header_urlstringOptional http(s) address the page name links to, e.g. the company website. Empty = the name is not a link.
hide_titlebooleanShow only the logo in the page header, without the page name (it stays the browser tab title). Ignored when the page has no logo.
logoPageLogoThe page logo, only in the answer of GET /api/status-pages/{id}. Change it with the logo routes.
publishedbooleanDrafts are visible only to logged-in users.
show_incidentsboolean
monitor_idsinteger[]
group_idsinteger[]Every monitor of these groups, current and future.
created_atintegerUnix time in milliseconds (0 = never / unknown).
subscribe_notifier_idintegerEmail 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.
protectedbooleanVisitors 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.
passwordstringNew password for visitors (6-72 characters), stored hashed and never returned. Needed when protected is set on a page without one.
example
{
"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"
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found · 409 already exists
delete/api/status-pages/{id}Delete a status pagewrite token · admin
parameters
id*path · integerId of the status page.
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
get/api/status-pages/{id}/incidentsList the incidents of a status pageread token

Incidents written on the page, open ones first, each with its updates (newest first).

parameters
id*path · integerId 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
}
]
}
]
}
errors: 401 not logged in, or the token is invalid or expired · 404 not found
post/api/status-pages/{id}/incidentsCreate an incident on a status pagewrite token · admin

Opens an incident with its first update. With notify (default true) the update is emailed to the confirmed subscribers of the page, in the background.

parameters
id*path · integerId of the status page.
request body · application/json
titlestringOne line, at most 200 characters. Required to create and to edit.
impactstringDefault minor.one of: none minor major critical
statusstringStatus of the update. Default investigating when creating.one of: investigating identified monitoring resolved
messagestringText of the update, at most 5000 characters. Required to create and for updates.
notifybooleanEmail the update to the confirmed subscribers of the page (default true). Ignored when editing the title or impact.
example
{
"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
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
put/api/status-pages/{id}/incidents/{incident}Edit an incidentwrite token · admin

Changes the title and impact. Updates are added with POST …/updates.

parameters
id*path · integerId of the status page.
incident*path · integerId of the incident.
request body · application/json
titlestringOne line, at most 200 characters. Required to create and to edit.
impactstringDefault minor.one of: none minor major critical
statusstringStatus of the update. Default investigating when creating.one of: investigating identified monitoring resolved
messagestringText of the update, at most 5000 characters. Required to create and for updates.
notifybooleanEmail the update to the confirmed subscribers of the page (default true). Ignored when editing the title or impact.
example
{
"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
}
]
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
delete/api/status-pages/{id}/incidents/{incident}Delete an incidentwrite token · admin

Removes the incident and all its updates from the page.

parameters
id*path · integerId of the status page.
incident*path · integerId of the incident.
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
post/api/status-pages/{id}/incidents/{incident}/updatesPost an incident updatewrite token · admin

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

parameters
id*path · integerId of the status page.
incident*path · integerId of the incident.
request body · application/json
titlestringOne line, at most 200 characters. Required to create and to edit.
impactstringDefault minor.one of: none minor major critical
statusstringStatus of the update. Default investigating when creating.one of: investigating identified monitoring resolved
messagestringText of the update, at most 5000 characters. Required to create and for updates.
notifybooleanEmail the update to the confirmed subscribers of the page (default true). Ignored when editing the title or impact.
example
{
"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
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
delete/api/status-pages/{id}/incidents/{incident}/updates/{update}Delete an incident updatewrite token · admin

Removes one update; the incident takes the status of the latest one left. The last update cannot be deleted (409): delete the incident instead.

parameters
id*path · integerId of the status page.
incident*path · integerId of the incident.
update*path · integerId 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
}
]
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found · 409 already exists
get/api/status-pages/{id}/subscribersList the email subscribers of a status pageadmin session

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

parameters
id*path · integerId of the status page.
response 200 · Subscribers.
{
"subscribers": [
{
"id": 1,
"page_id": 1,
"email": "string",
"confirmed_at": 1,
"created_at": 1761868800000
}
],
"enabled": true
}
errors: 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
delete/api/status-pages/{id}/subscribers/{subscriber}Remove an email subscriberadmin session

The address gets no more emails from the page.

parameters
id*path · integerId of the status page.
subscriber*path · integerId of the subscriber.
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
post/api/status-pages/previewPreview a status pagewrite token · admin

What visitors would see with these settings, without saving them. With the id of a saved page, the incidents written on it are included.

request body · application/json
idinteger
slug*stringURL path: /status/<slug>. 1-50 lowercase letters, digits or dashes.
title*string
descriptionstring
domainstringOptional custom domain serving the page, e.g. status.example.com.
header_urlstringOptional http(s) address the page name links to, e.g. the company website. Empty = the name is not a link.
hide_titlebooleanShow only the logo in the page header, without the page name (it stays the browser tab title). Ignored when the page has no logo.
logoPageLogoThe page logo, only in the answer of GET /api/status-pages/{id}. Change it with the logo routes.
publishedbooleanDrafts are visible only to logged-in users.
show_incidentsboolean
monitor_idsinteger[]
group_idsinteger[]Every monitor of these groups, current and future.
created_atintegerUnix time in milliseconds (0 = never / unknown).
subscribe_notifier_idintegerEmail 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.
protectedbooleanVisitors 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.
passwordstringNew password for visitors (6-72 characters), stored hashed and never returned. Needed when protected is set on a page without one.
example
{
"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
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication

──system

Health, counters, metrics and this document.

get/api/docsInteractive API docspublic

Redirects to the Swagger UI page of the web app.

errors: 302 redirect to /api-docs/
get/api/healthHealth checkpublic

Liveness probe, also returns the hub version.

response 200 · Hub is running.
{
"ok": true,
"version": "v0.1.0"
}
get/api/openapi.jsonOpenAPI documentpublic

This document, for the running version.

response 200 · OpenAPI 3.1 document.
{}
get/api/overviewDashboard countersread token
response 200 · Counters.
{
"monitors": 12,
"status": {
"up": 10,
"down": 1,
"paused": 1
},
"agents": 2,
"agents_online": 2,
"open_incidents": 1,
"version": "v0.1.0"
}
errors: 401 not logged in, or the token is invalid or expired
get/api/telemetryAnonymous usage statsadmin session
response 200 · Usage stats status.
{
"enabled": true,
"asked": true,
"forced_off": true,
"report": null,
"sent_at": 1,
"error": "string"
}
errors: 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
put/api/telemetryTurn anonymous usage stats on or offadmin session
request body · application/json
enabled*boolean
example
{
"enabled": true
}
response 200 · Usage stats status.
{
"enabled": true,
"asked": true,
"forced_off": true,
"report": null,
"sent_at": 1,
"error": "string"
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 409 usage stats are turned off on this hub (pingtxt_disable_telemetry or a development build)
get/api/versionVersion and update checkread token
response 200 · Version status.
{
"current": "string",
"check_enabled": true,
"check_disabled": true,
"latest": null,
"update_available": true,
"checked_at": 1,
"error": "string"
}
errors: 401 not logged in, or the token is invalid or expired
post/api/version/checkCheck for a new version nowadmin session
response 200 · Version status.
{
"current": "string",
"check_enabled": true,
"check_disabled": true,
"latest": null,
"update_available": true,
"checked_at": 1,
"error": "string"
}
errors: 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 409 the check is off (settings, or a development build)
get/metricsPrometheus metricsread token

Metrics 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
errors: 401 not logged in, or the token is invalid or expired

──auth

Setup, login, password, single sign-on and two-factor. Browser oriented.

post/api/auth/2fa/disableDisable two-factorbrowser session
request body · application/json
code*stringTOTP or recovery code.
example
{
"code": "string"
}
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 429 too many failed attempts. password logins are limited to 10 failures per account and 30 per client address in 15 minutes; second factor codes to 10 wrong codes per user. wait for `retry-after` seconds
post/api/auth/2fa/enableEnable two-factorbrowser session
request body · application/json
code*string
example
{
"code": "123456"
}
response 200 · Enabled. Recovery codes are shown once.
{
"recovery_codes": [
"string"
]
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
post/api/auth/2fa/recovery-codesNew recovery codesbrowser session

Replaces the old codes.

request body · application/json
code*string
example
{
"code": "string"
}
response 200 · New codes.
{
"recovery_codes": [
"string"
]
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 429 too many failed attempts. password logins are limited to 10 failures per account and 30 per client address in 15 minutes; second factor codes to 10 wrong codes per user. wait for `retry-after` seconds
post/api/auth/2fa/setupStart two-factor setupbrowser session

Returns 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"
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
delete/api/auth/identities/{provider}Unlink a single sign-on providerbrowser session
parameters
provider*path · stringone of: github google oidc
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
post/api/auth/loginLog in with passwordpublic

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

request body · application/json
email*string
password*string
example
{
"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
}
]
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 429 too many failed attempts. password logins are limited to 10 failures per account and 30 per client address in 15 minutes; second factor codes to 10 wrong codes per user. wait for `retry-after` seconds
post/api/auth/login/2faComplete a two-factor loginpublic
request body · application/json
challenge*string
code*string6 digit TOTP code or a recovery code.
example
{
"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
}
]
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 429 too many failed attempts. password logins are limited to 10 failures per account and 30 per client address in 15 minutes; second factor codes to 10 wrong codes per user. wait for `retry-after` seconds
post/api/auth/logoutLog outpublic

Ends the browser session.

response 200 · Done.
{
"ok": true
}
get/api/auth/meCurrent userread token
response 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
}
errors: 401 not logged in, or the token is invalid or expired
get/api/auth/oauth/{provider}/callbackSingle sign-on callbackpublic

Register <public url>/api/auth/oauth/<provider>/callback at the provider. Not meant to be called directly.

parameters
provider*path · stringone of: github google oidc
errors: 302 redirect to the app
get/api/auth/oauth/{provider}/startStart single sign-onpublic

Browser redirect to the provider. ?next=/path returns there after login, ?link=1 links the provider to the logged-in account.

parameters
provider*path · stringone of: github google oidc
nextquery · stringLocal path to open after login.
linkquery · string1 to link to the current account.
errors: 302 redirect to the provider
get/api/auth/optionsLogin optionspublic

What the login page shows: password form and single sign-on buttons.

response 200 · Options.
{
"password_login": true,
"providers": [
{
"id": "github",
"label": "GitHub"
}
]
}
post/api/auth/passwordChange passwordbrowser session

Other sessions of the user are logged out. Accounts created with single sign-on can set a first password with an empty current.

request body · application/json
currentstring
new*stringAt least 8 characters.
example
{
"current": "string",
"new": "string"
}
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
get/api/setupSetup statepublic

Whether the instance still needs its first admin account.

response 200 · State.
{
"needs_setup": false,
"version": "v0.1.0"
}
post/api/setupCreate the first adminpublic

Works only while no user exists. Starts a session.

request body · application/json
email*string
password*stringAt least 8 characters.
example
{
"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": []
}
errors: 400 invalid input · 409 already exists

──api tokens

Personal tokens and connected OAuth apps. Browser session only.

get/api/oauth/grantsList connected appsbrowser session

Apps 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"
}
]
errors: 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
delete/api/oauth/grants/{id}Disconnect an appbrowser session
parameters
id*path · integerId of the grant.
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
get/api/tokensList your API tokensbrowser session
response 200 · Tokens of the current user.
[
{
"id": 1,
"name": "string",
"hint": "string",
"scope": "read",
"created_at": 1761868800000,
"last_used_at": 1761868800000,
"expires_at": 1
}
]
errors: 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
post/api/tokensCreate an API tokenbrowser session

The secret is returned once. Read-only users can only create read tokens. At most 50 tokens per user.

request body · application/json
name*stringUp to 100 characters.
scope*stringone of: read write
expires_in_daysinteger0 = never, up to 3650.
example
{
"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"
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
delete/api/tokens/{id}Revoke an API tokenbrowser session
parameters
id*path · integerId of the token.
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found

──users

Accounts and roles. Admin browser session only.

get/api/usersList usersadmin session
response 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
}
]
}
]
errors: 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
post/api/usersCreate a useradmin session

Without a password the user logs in with single sign-on (matched by verified email).

request body · application/json
email*string
passwordstringOptional, at least 8 characters.
role*stringone of: admin viewer
example
{
"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
}
]
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 409 already exists
put/api/users/{id}Change a user's roleadmin session

You cannot change your own role.

parameters
id*path · integerId of the user.
request body · application/json
role*stringone of: admin viewer
example
{
"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
}
]
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
delete/api/users/{id}Delete a useradmin session
parameters
id*path · integerId of the user.
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
delete/api/users/{id}/2faReset a user's two-factoradmin session
parameters
id*path · integerId of the user.
response 200 · Done.
{
"ok": true
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found

──settings

Instance settings. Admin browser session only.

get/api/settingsGet instance settingsadmin session
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"
}
errors: 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
put/api/settings/authSave login settingsadmin session
request body · application/json
password_loginboolean
allow_signupbooleanCreate accounts on first single sign-on login.
allowed_domainsstring[]Email domains allowed to sign up, empty = any.
default_rolestringone of: admin viewer
providersobjectKeyed by github, google, oidc.
example
{
"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"
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
put/api/settings/generalSave general settingsadmin session
request body · application/json
public_urlstringPublic URL of the hub, used in links, agent setup and OAuth callbacks.
local_namestringName of the hub's own location.
local_enabledbooleanThe hub runs checks itself.
retention_daysintegerHourly history, up to 3650.
raw_retention_daysintegerEvery single check.
monitor_defaultsobject
expiryobject
metricsobject
degraded_pctintegerDefault degraded threshold, 1 to 50.
ip_familyintegerDefault 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_checkobject
example
{
"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"
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
get/api/settings/pushGet mobile app push settingsadmin session
response 200 · Push settings.
{
"enabled": true,
"relay_url": "string",
"hub_id": "string",
"registered": true,
"devices": 1
}
errors: 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
put/api/settings/pushSave mobile app push settingsadmin session

Turning 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).

request body · application/json
enabledboolean
relay_urlstring
example
{
"enabled": true,
"relay_url": "https://push.pingtxt.com"
}
response 200 · Saved.
{
"enabled": true,
"relay_url": "string",
"hub_id": "string",
"registered": true,
"devices": 1
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 502 the notification could not be delivered
post/api/settings/push/testTest the push relayadmin session

Checks 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"
}
errors: 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 409 already exists · 502 the notification could not be delivered

──backups

Database backups and configuration export/import.

get/api/backupsList backupsadmin session
response 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
}
errors: 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
post/api/backupsBack up nowadmin session
response 201 · Backup file.
{
"name": "string",
"kind": "auto",
"size": 1,
"created_at": 1761868800000
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
get/api/backups/{name}Download a backupadmin session
parameters
name*path · stringBackup file name.
response 200 · The file.
errors: 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
delete/api/backups/{name}Delete a backupadmin session
parameters
name*path · stringBackup file name.
response 200 · Done.
{
"ok": true
}
errors: 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
post/api/backups/{name}/restoreStage a restoreadmin session

The backup replaces the database at the next start of the hub.

parameters
name*path · stringBackup file name.
response 200 · Staged.
{
"pending_restore": {
"name": "string",
"staged_at": 1761868800000
},
"info": {
"schema_version": 1,
"monitors": 1,
"users": 1
}
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication · 404 not found
delete/api/backups/restoreCancel a staged restoreadmin session
response 200 · Done.
{
"ok": true
}
errors: 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
post/api/backups/uploadUpload a backupadmin session

Raw SQLite file as the body (Content-Type: application/octet-stream), up to 4 GB. SQLite only.

request body · application/octet-stream
response 201 · Stored.
{
"backup": {
"name": "string",
"kind": "auto",
"size": 1,
"created_at": 1761868800000
},
"info": {
"schema_version": 1,
"monitors": 1,
"users": 1
}
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
get/api/config/exportExport the configurationread token

Admins only. Without secrets by default; ?secrets=1 needs a browser session (API tokens get 403).

parameters
secretsquery · string1 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": [
{}
]
}
errors: 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
post/api/config/importImport a configurationadmin session

A backup is taken first. Body up to 64 MB.

parameters
modequery · stringreplace deletes what is not in the document.one of: merge replace
settingsquery · string0 to leave instance settings alone.
request body · application/json
formatstringAlways "pingtxt-config".
versioninteger
hub_versionstring
exported_atstring
secretsbooleanPasswords and tokens are included.
settingsobject
agentsobject[]
groupsobject[]
tagsobject[]
notifiersobject[]
monitorsobject[]
maintenancesobject[]
status_pagesobject[]
example
{
"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"
]
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
post/api/import/uptime-kumaImport from Uptime Kumaadmin session

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

parameters
dry_runquery · booleanPreview only.
request body · application/json
versionstring
monitorList*object[]
example
{
"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
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication
put/api/settings/backupsSave backup scheduleadmin session
request body · application/json
enabledboolean
interval_hoursinteger
keepintegerScheduled backups to keep.
example
{
"enabled": true,
"interval_hours": 24,
"keep": 7
}
response 200 · Saved.
{
"enabled": true,
"interval_hours": 1,
"keep": 1
}
errors: 400 invalid input · 401 not logged in, or the token is invalid or expired · 403 not allowed for this role, token scope or kind of authentication

──oauth

OAuth 2.1 authorization server for native apps and MCP clients. Form bodies, OAuth error format.

get/.well-known/oauth-authorization-serverAuthorization server metadatapublic

RFC 8414 metadata: endpoints, scopes, PKCE methods.

response 200 · Metadata.
{}
get/oauth/authorizeStart a sign-inpublic

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

parameters
response_type*query · stringcode.
client_id*query · stringpingtxt-ios for the iOS app.
redirect_uri*query · stringpingtxt://oauth for the iOS app.
code_challenge*query · stringBASE64URL(SHA256(code_verifier)), no padding.
code_challenge_method*query · stringS256.
state*query · stringRandom value, checked on return.
scopequery · stringread, write or read write: write is preselected on the consent page for admins.
resourcequery · stringOptional (RFC 8707): <hub>/api or <hub>/mcp.
response 200 · Consent page (HTML).
errors: 302 to the login page, or back to `redirect_uri` · 400 unknown client or redirect uri not registered (html, no redirect)
post/oauth/revokeSign outpublic

Ends the sign-in of the token (access or refresh token, RFC 7009). Always answers 200.

request body · application/x-www-form-urlencoded
token*stringRefresh or access token.
client_id*stringpingtxt-ios for the iOS app.
response 200 · Done (also when the token was unknown).
post/oauth/tokenGet or refresh tokenspublic

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

request body · application/x-www-form-urlencoded
grant_type*stringauthorization_code or refresh_token.
client_id*stringpingtxt-ios for the iOS app.
codestringauthorization_code: the code from the redirect.
redirect_uristringauthorization_code: same as in the authorization request.
code_verifierstringauthorization_code: the PKCE verifier.
refresh_tokenstringrefresh_token: the latest refresh token.
response 200 · Tokens.
{
"access_token": "string",
"token_type": "Bearer",
"expires_in": 1,
"refresh_token": "string",
"scope": "read"
}
errors: 400 oauth error