Account API reference#
The read-only, customer-facing JSON API for a person’s own account data —
checks, monitors, results, heartbeats, notification channels, members, and
plan settings — plus a full data-export endpoint. This is a different
surface from the monitor API: that one is what a
monitor agent uses to talk to the server, authenticated as a monitor;
this one is what a person (or a script acting on their behalf) uses to
read their own org’s data, authenticated with their own bearer token
minted from the organisation settings page. Generated from
docs-site/assets/tenpmuptime-account.yaml — see that file’s header for
how it’s kept in sync with the code.
The read-only, customer-facing JSON API for a person's own account data - checks, monitors, results, heartbeats, notification channels, members, and plan settings - plus a full data-export endpoint. This is a different surface from the monitor sync API documented in tenpmuptime-monitor.yaml: that one is what a monitor agent uses to talk to the server, authenticated as a monitor; this one is what a person (or a script acting on their behalf) uses to read their own org's data, authenticated with their own bearer token. The wire contract is implemented by internal/server/account_api_handlers.go and account_api.go; treat this document as generated-by-hand from that code, not the other way around - if the two disagree, the code is right and this file is stale.
Read-only in this pass. There is no way to create or edit a check, monitor, or anything else through this API yet - every route below is a GET. See account-api-design.md's "Open items" for why a write path is deferred rather than built partially.
Authentication. Every route requires a personal access token (Authorization: Bearer tpk_...), scoped to exactly one organisation. There is no endpoint in this API to mint one - a token is minted from the browser UI instead, at POST /settings/api-tokens on the organisation settings page (session-authed, CSRF-protected, not part of this API), and only by that org's owner. The plaintext is shown exactly once, on the page that minted it; only its hash is ever stored, so a lost token can only be revoked and re-minted, never recovered. Minting is refused outright during an admin "login as" impersonation session, so a one-hour support session can never leave behind a standing credential. Membership is re-proved on every request (the same continuous check a browser session gets): if the user who minted a token is later removed from the org, the token stops working immediately, with no separate revocation step required.
Error responses across every endpoint are a plain-text body (one line) with a non-2xx status, not a JSON envelope - same convention the monitor API uses. The 4xx/5xx responses below document status code and body meaning; there is no shared error schema.
Rate limits, two layers. An outer, client-address-keyed limit (shared with the monitor API's own route table entry) catches unauthenticated noise before any database read happens. The real, per-customer budget is applied per token id once the token is resolved: 5 requests/second, burst 20, for every route except GET /account-api/v1/export, which - because it alone walks a check's entire retained result history - has its own, far tighter budget of once a minute, burst 2, on top of the general one. Exceeding either answers 429 rate limit exceeded.
Versioning. Paths are versioned from the start (/account-api/v1/), landed 2026-09-27 alongside the monitor API's own move to /monitor-api/v1/ - the two were given distinct prefixes rather than sharing one /api/ root specifically because this API's /checks//checks/{guid} would otherwise collide with the monitor API's own /checks. The same additive-only rule applies here: a JSON change within v1 only ever adds a field; a removed field or a changed meaning gets /account-api/v2/ instead.
v1.0.0
Servers#
- Local dev server
http://127.0.0.1:8002
Security#
- A person's own personal access token, prefixed
tpk_in its plaintext form so it's recognizable on sight in a log line or a secret scanner. Minted from the browser UI atPOST /settings/api-tokens(not part of this API), by the org's owner only, shown exactly once. Scoped to one organisation; the server re-proves the minting user's membership in that org on every request, so removing them from the org silently invalidates every token they minted with no separate revocation step. accountAPIKeyAuthhttpbearer
API#
List this org's checks#
Every check in the org, in the same shape as the check import/export file (checkExport) - the account API's check resource and the existing GET /checks/export format are deliberately the same shape.
Request (requires accountAPIKeyAuth)#
GET /account-api/v1/checksResponses#
200: The org's checks.
Schema: array of Check
| Property | Type | Required | Description |
|---|---|---|---|
cert_expiry_warn_days | integer | no | For https/tls checks. 0/absent means no such constraint; the boundary itself fails (14 set fails at exactly 14 days left). |
disable_redirects | boolean | no | |
down_interval_sec | integer | no | How many consecutive failures before this check is considered down and an alert fires. |
enabled | boolean | yes | |
guid | string | yes | |
headers | object | no | |
insecure_skip_verify | boolean | no | |
interval_sec | integer | yes | How often the check runs in total, across every monitor assigned to it. |
invert_result | boolean | no | "Upside-down mode": flips the final pass/fail verdict. |
match_mode | string | no | Absent means contains. |
match_string | string | yes | |
max_response_time_ms | integer | no | 0/absent means no such constraint. |
name | string | yes | |
post_data | string | no | |
resend_interval_sec | integer | no | How often a still-failing check repeats its DOWN notification; 0/absent means never. |
restricted | boolean | yes | Whether this check is restricted to the org's own monitors (opted in explicitly, or derived from the target being an internal/non-public address). |
result_detail_max_chars | integer | yes | Caps how many characters of a result's error/ response_sample this check's results may carry. Always present (0 is a meaningful "send nothing", not "absent"). |
status_code_op | string | no | Absent means the built-in "status >= 400 fails" rule, not "no constraint". |
status_code_value | integer | no | |
timeout_sec | integer | no | 0/absent means the default (10s). |
url | string | yes |
401: Missing/malformed Authorization header, the token is unknown or revoked, the minting user's account has been disabled, or that user is no longer a member of the token's org.
Schema: string
429: Rate limit exceeded (per-token or per-client-address).
Schema: string
Get one check#
Request (requires accountAPIKeyAuth)#
GET /account-api/v1/checks/{guid}Path parameters#
| Parameter name | Value | Description | Additional |
|---|---|---|---|
guid | string | Required |
Responses#
200: The check.
Schema: Check
| Property | Type | Required | Description |
|---|---|---|---|
cert_expiry_warn_days | integer | no | For https/tls checks. 0/absent means no such constraint; the boundary itself fails (14 set fails at exactly 14 days left). |
disable_redirects | boolean | no | |
down_interval_sec | integer | no | How many consecutive failures before this check is considered down and an alert fires. |
enabled | boolean | yes | |
guid | string | yes | |
headers | object | no | |
insecure_skip_verify | boolean | no | |
interval_sec | integer | yes | How often the check runs in total, across every monitor assigned to it. |
invert_result | boolean | no | "Upside-down mode": flips the final pass/fail verdict. |
match_mode | string | no | Absent means contains. |
match_string | string | yes | |
max_response_time_ms | integer | no | 0/absent means no such constraint. |
name | string | yes | |
post_data | string | no | |
resend_interval_sec | integer | no | How often a still-failing check repeats its DOWN notification; 0/absent means never. |
restricted | boolean | yes | Whether this check is restricted to the org's own monitors (opted in explicitly, or derived from the target being an internal/non-public address). |
result_detail_max_chars | integer | yes | Caps how many characters of a result's error/ response_sample this check's results may carry. Always present (0 is a meaningful "send nothing", not "absent"). |
status_code_op | string | no | Absent means the built-in "status >= 400 fails" rule, not "no constraint". |
status_code_value | integer | no | |
timeout_sec | integer | no | 0/absent means the default (10s). |
url | string | yes |
401: See GET /account-api/v1/checks.
Schema: string
404: No check with this guid in the token's org.
Schema: string
429: Rate limit exceeded.
Schema: string
Page through a check's results, oldest first#
Keyset-paginated, not offset: after/after_monitor anchor on the last row a previous page actually returned ((ran_at, monitor_id), unique per check), rather than a row count that shifts underneath a caller as the results table keeps growing. Keep paging with the response's own next cursor until a page comes back with an empty results and no next.
Request (requires accountAPIKeyAuth)#
GET /account-api/v1/checks/{guid}/resultsPath parameters#
| Parameter name | Value | Description | Additional |
|---|---|---|---|
guid | string | Required |
Query parameters#
| Parameter name | Value | Description | Additional |
|---|---|---|---|
after | string (date-time) | RFC3339 timestamp. Omit to start from the beginning. Must be a value taken from a previous page's next.after, paired with after_monitor from the same cursor. | |
after_monitor | string | The monitor id half of the same cursor as after. |
Responses#
200: One page of results.
Schema: CheckResultsPage
| Property | Type | Required | Description |
|---|---|---|---|
next | no | Present whenever this page returned any rows. Absent (both this field and an empty results) means there is nothing more to page. | |
results | array of Result | yes |
400: after was present but not a valid RFC3339 timestamp.
Schema: string
401: See GET /account-api/v1/checks.
Schema: string
404: No check with this guid in the token's org.
Schema: string
429: Rate limit exceeded.
Schema: string
Export this org's full data as one JSON document#
The mechanism behind a customer data-export request (GDPR Art. 20 / CCPA "right to know"). Everything the other endpoints in this API can return, plus every retained result for every check, in one response - scoped to this org's own data, not a full personal-data export of the requesting user themselves (see account-api-design.md's "Open items" for that gap).
The header section (org name, entitlements, plan usage, checks, monitors, heartbeats, notification channels, members) is small and bounded by the org's own entitlements, so it is built and returned whole. The results section is streamed: one bracketed array per check, keyed by the check's guid, written directly from the same keyset cursor GET .../checks/{guid}/results uses - a Team-tier org's full raw-retention history can reach tens of millions of rows, too large to hold in memory as both a Go slice and its own JSON encoding at once. The accepted tradeoff: once the 200 and part of the body are already on the wire, a mid-stream store error can only be logged and the connection closed, leaving a truncated (invalid) JSON document as the only signal - a client should treat a response that fails to parse as a failed export to retry, not a partial success to salvage.
Deliberately excluded from the export: each result's own alert/notification delivery history - that describes this server's notification attempts, not the check's data.
Response is served with Content-Disposition: attachment so a browser hitting this URL directly downloads a file rather than rendering it inline.
Request (requires accountAPIKeyAuth)#
GET /account-api/v1/exportResponses#
200: The full export document: {...AccountExportHeader fields, "results": {"<check guid>": [Result, ...], ...}}. See the description above for the streaming caveat - a response that doesn't parse as valid JSON should be treated as a failed export.
Schema: AccountExport
| Property | Type | Required | Description |
|---|---|---|---|
checks | array of Check | yes | |
entitlements | Entitlements | yes | This org's plan limits (billing-plans-design.md). Serialized with Go's default field-name-as-key behavior - this type carries no json tags of its own, since it's shared as-is with the admin UI and the tenant plan page rather than given a second, API-specific shape. |
generated_at | string (date-time) | yes | |
heartbeats | array of Heartbeat | yes | |
members | array of Member | yes | |
monitors | array of Monitor | yes | |
notification_channels | array of NotificationChannel | yes | |
org_name | string | yes | |
plan_usage | PlanUsage | yes | A snapshot of how much of each entitlement above this org actually uses right now. Same no-json-tags treatment as Entitlements. |
results | object | yes | Keyed by each check's own guid; each value is the full array of that check's retained results, oldest first, with no pagination (unlike GET .../checks/{guid}/results, this endpoint walks every page internally and streams the concatenated result). |
401: See GET /account-api/v1/checks.
Schema: string
429: Rate limit exceeded. /export has its own much tighter budget (once a minute, burst 2) than every other route in this API, independent of and in addition to the general per-token budget.
Schema: string
List this org's heartbeat checks#
Never includes a heartbeat's own ingest token - that credential is shown once, on the tenant detail page, and is not re-derivable from a listing anywhere, including here.
Request (requires accountAPIKeyAuth)#
GET /account-api/v1/heartbeatsResponses#
200: The org's heartbeats.
Schema: array of Heartbeat
| Property | Type | Required | Description |
|---|---|---|---|
down_since | string (date-time) | no | |
enabled | boolean | yes | True only when both the owner's own enabled bit and the platform's kill switch (admin_enabled) are true. |
grace_sec | integer | yes | Extra time allowed past interval_sec before a missed beat is considered a failure. |
guid | string | yes | |
interval_sec | integer | yes | |
last_heartbeat_at | string (date-time) | no | |
name | string | yes | |
resend_interval_sec | integer | no | How often a still-down heartbeat repeats its DOWN notification; 0/absent means never. |
show_on_status_page | boolean | yes | |
state | string | yes | The stored state. Note this can read stale/misleading when enabled is false - see Heartbeat.Status()'s own reasoning: a disabled or paused heartbeat is never swept, so its stored state is not a live fact. |
401: See GET /account-api/v1/checks.
Schema: string
429: Rate limit exceeded.
Schema: string
List this org's members#
Request (requires accountAPIKeyAuth)#
GET /account-api/v1/membersResponses#
200: The org's members, owner first, then alphabetically by email.
Schema: array of Member
| Property | Type | Required | Description |
|---|---|---|---|
email | string | yes | |
id | integer (int64) | yes | |
is_owner | boolean | yes | |
joined_at | string (date-time) | yes |
401: See GET /account-api/v1/checks.
Schema: string
429: Rate limit exceeded.
Schema: string
List monitors available to this org#
This org's own monitors (enabled or not) plus every currently enabled shared-fleet monitor - the same set the tenant Monitors page shows (ListMonitorsAndSharedFleet).
Request (requires accountAPIKeyAuth)#
GET /account-api/v1/monitorsResponses#
200: The monitor list.
Schema: array of Monitor
| Property | Type | Required | Description |
|---|---|---|---|
city | string | yes | |
class | string | yes | |
country | string | yes | |
enabled | boolean | yes | True only when both the org's own enabled bit and the platform's own kill switch (admin_enabled) are true - the same AND-of-both-flags convention the monitor sync API reports to an agent as its own single enabled field. |
id | string | yes | Globally unique across every org and the shared fleet. |
name | string | yes | |
region | string | yes |
401: See GET /account-api/v1/checks.
Schema: string
429: Rate limit exceeded.
Schema: string
List this org's notification channels#
Includes each channel's real destination/secret (ntfy_topic/email/webhook_url/webhook_secret) unredacted - only the org's owner can mint the token that reaches this endpoint, and the owner can already read every one of these values from the organisation settings page, so redacting them here would protect nothing.
Request (requires accountAPIKeyAuth)#
GET /account-api/v1/notification-channelsResponses#
200: The org's notification channels.
Schema: array of NotificationChannel
| Property | Type | Required | Description |
|---|---|---|---|
email | string | no | |
enabled | boolean | yes | |
id | integer (int64) | yes | |
kind | string | yes | |
name | string | yes | |
ntfy_topic | string | no | |
webhook_secret | string | no | Meaningful only for the webhook kind. Empty is the deliberate "send unsigned" state, not an omitted field. |
webhook_url | string | no | The destination for both webhook and slack kinds. |
401: See GET /account-api/v1/checks.
Schema: string
429: Rate limit exceeded.
Schema: string
Get this org's name, plan entitlements, and current usage#
Request (requires accountAPIKeyAuth)#
GET /account-api/v1/settingsResponses#
200: The org's settings.
Schema: AccountSettings
| Property | Type | Required | Description |
|---|---|---|---|
entitlements | Entitlements | yes | This org's plan limits (billing-plans-design.md). Serialized with Go's default field-name-as-key behavior - this type carries no json tags of its own, since it's shared as-is with the admin UI and the tenant plan page rather than given a second, API-specific shape. |
org_name | string | yes | |
plan_usage | PlanUsage | yes | A snapshot of how much of each entitlement above this org actually uses right now. Same no-json-tags treatment as Entitlements. |
401: See GET /account-api/v1/checks.
Schema: string
429: Rate limit exceeded.
Schema: string
Models#
AccountExport#
The full export document: every other resource in this API plus a results object keyed by check guid. See GET /account-api/v1/export's own description for the streaming caveat that makes this shape hand-assembled rather than a single marshaled struct.
| Property | Type | Required | Description |
|---|---|---|---|
checks | array of Check | yes | |
entitlements | Entitlements | yes | This org's plan limits (billing-plans-design.md). Serialized with Go's default field-name-as-key behavior - this type carries no json tags of its own, since it's shared as-is with the admin UI and the tenant plan page rather than given a second, API-specific shape. |
generated_at | string (date-time) | yes | |
heartbeats | array of Heartbeat | yes | |
members | array of Member | yes | |
monitors | array of Monitor | yes | |
notification_channels | array of NotificationChannel | yes | |
org_name | string | yes | |
plan_usage | PlanUsage | yes | A snapshot of how much of each entitlement above this org actually uses right now. Same no-json-tags treatment as Entitlements. |
results | object | yes | Keyed by each check's own guid; each value is the full array of that check's retained results, oldest first, with no pagination (unlike GET .../checks/{guid}/results, this endpoint walks every page internally and streams the concatenated result). |
AccountSettings#
| Property | Type | Required | Description |
|---|---|---|---|
entitlements | Entitlements | yes | This org's plan limits (billing-plans-design.md). Serialized with Go's default field-name-as-key behavior - this type carries no json tags of its own, since it's shared as-is with the admin UI and the tenant plan page rather than given a second, API-specific shape. |
org_name | string | yes | |
plan_usage | PlanUsage | yes | A snapshot of how much of each entitlement above this org actually uses right now. Same no-json-tags treatment as Entitlements. |
Check#
The same shape as GET /checks/export's file format (checkExport) - the account API's check resource is that format, not a new one. For a private-definition check (one whose definition has been wiped from the server - see private-checks-design.md), url/match_string/post_data/headers and the other content-group fields are empty/absent, the same treatment the monitor sync API gives such a check.
| Property | Type | Required | Description |
|---|---|---|---|
cert_expiry_warn_days | integer | no | For https/tls checks. 0/absent means no such constraint; the boundary itself fails (14 set fails at exactly 14 days left). |
disable_redirects | boolean | no | |
down_interval_sec | integer | no | How many consecutive failures before this check is considered down and an alert fires. |
enabled | boolean | yes | |
guid | string | yes | |
headers | object | no | |
insecure_skip_verify | boolean | no | |
interval_sec | integer | yes | How often the check runs in total, across every monitor assigned to it. |
invert_result | boolean | no | "Upside-down mode": flips the final pass/fail verdict. |
match_mode | string | no | Absent means contains. |
match_string | string | yes | |
max_response_time_ms | integer | no | 0/absent means no such constraint. |
name | string | yes | |
post_data | string | no | |
resend_interval_sec | integer | no | How often a still-failing check repeats its DOWN notification; 0/absent means never. |
restricted | boolean | yes | Whether this check is restricted to the org's own monitors (opted in explicitly, or derived from the target being an internal/non-public address). |
result_detail_max_chars | integer | yes | Caps how many characters of a result's error/ response_sample this check's results may carry. Always present (0 is a meaningful "send nothing", not "absent"). |
status_code_op | string | no | Absent means the built-in "status >= 400 fails" rule, not "no constraint". |
status_code_value | integer | no | |
timeout_sec | integer | no | 0/absent means the default (10s). |
url | string | yes |
CheckResultsPage#
| Property | Type | Required | Description |
|---|---|---|---|
next | no | Present whenever this page returned any rows. Absent (both this field and an empty results) means there is nothing more to page. | |
results | array of Result | yes |
Entitlements#
This org's plan limits (billing-plans-design.md). Serialized with Go's default field-name-as-key behavior - this type carries no json tags of its own, since it's shared as-is with the admin UI and the tenant plan page rather than given a second, API-specific shape.
| Property | Type | Required | Description |
|---|---|---|---|
AllowChannelEmail | boolean | no | |
AllowChannelNtfy | boolean | no | |
AllowChannelSlack | boolean | no | |
AllowChannelWebhook | boolean | no | |
AllowStatusPage | boolean | no | |
CustomPriceCents | integer | no | |
CustomPricePeriod | string | no | Empty means no price has been recorded, independent of Plan. |
MaxChecks | integer | no | 0 = unlimited. |
MaxHeartbeats | integer | no | 0 means the plan includes no heartbeats at all - unlike the other Max* fields here, there is no unlimited value for this one. |
MaxMembers | integer | no | 0 = unlimited. |
MaxNotificationChannels | integer | no | 0 = unlimited. |
MaxTimeoutSec | integer | no | |
MinIntervalSec | integer | no | |
Plan | string | no | |
RetentionRawDays | integer | no | |
RetentionRollupDays | integer | no |
Heartbeat#
| Property | Type | Required | Description |
|---|---|---|---|
down_since | string (date-time) | no | |
enabled | boolean | yes | True only when both the owner's own enabled bit and the platform's kill switch (admin_enabled) are true. |
grace_sec | integer | yes | Extra time allowed past interval_sec before a missed beat is considered a failure. |
guid | string | yes | |
interval_sec | integer | yes | |
last_heartbeat_at | string (date-time) | no | |
name | string | yes | |
resend_interval_sec | integer | no | How often a still-down heartbeat repeats its DOWN notification; 0/absent means never. |
show_on_status_page | boolean | yes | |
state | string | yes | The stored state. Note this can read stale/misleading when enabled is false - see Heartbeat.Status()'s own reasoning: a disabled or paused heartbeat is never swept, so its stored state is not a live fact. |
Member#
| Property | Type | Required | Description |
|---|---|---|---|
email | string | yes | |
id | integer (int64) | yes | |
is_owner | boolean | yes | |
joined_at | string (date-time) | yes |
Monitor#
| Property | Type | Required | Description |
|---|---|---|---|
city | string | yes | |
class | string | yes | |
country | string | yes | |
enabled | boolean | yes | True only when both the org's own enabled bit and the platform's own kill switch (admin_enabled) are true - the same AND-of-both-flags convention the monitor sync API reports to an agent as its own single enabled field. |
id | string | yes | Globally unique across every org and the shared fleet. |
name | string | yes | |
region | string | yes |
NotificationChannel#
| Property | Type | Required | Description |
|---|---|---|---|
email | string | no | |
enabled | boolean | yes | |
id | integer (int64) | yes | |
kind | string | yes | |
name | string | yes | |
ntfy_topic | string | no | |
webhook_secret | string | no | Meaningful only for the webhook kind. Empty is the deliberate "send unsigned" state, not an omitted field. |
webhook_url | string | no | The destination for both webhook and slack kinds. |
PlanUsage#
A snapshot of how much of each entitlement above this org actually uses right now. Same no-json-tags treatment as Entitlements.
| Property | Type | Required | Description |
|---|---|---|---|
Checks | integer | no | |
ChecksWithCertExpiry | integer | no | |
Heartbeats | integer | no | |
MaxTimeoutSec | integer | no | The loosest timeout actually configured; 0 when Checks is 0. |
Members | integer | no | |
MinIntervalSec | integer | no | The tightest interval actually configured across this org's checks; 0 when Checks is 0. |
NotificationChannels | integer | no | |
StatusPageEnabled | boolean | no | The org's own current setting, independent of whether the plan allows it. |
UsedChannelKindsLabel | string | no | Human-readable, comma-separated list of channel kinds this org has actually created (not which kinds the plan permits). |
Result#
One check execution outcome. Deliberately narrower than the store's internal result row - no alert-delivery fields.
| Property | Type | Required | Description |
|---|---|---|---|
error | string | no | Empty on success. |
http_status | integer | no | |
latency_ms | integer | yes | |
monitor_id | string | yes | |
monitor_name | string | no | Empty if the monitor was enrolled with no name. |
ran_at | string (date-time) | yes | |
response_sample | string | no | Leading characters of what the target sent back, capped at the check's own result_detail_max_chars. |
success | boolean | yes |
ResultsCursor#
| Property | Type | Required | Description |
|---|---|---|---|
after | string (date-time) | yes | |
after_monitor | string | yes |