Monitor API reference#

The JSON API a monitor agent uses to enroll with a tenpm uptime server, sync its assigned check set, and report results and status back. Generated from docs-site/assets/tenpmuptime-monitor.yaml — see that file’s header for how it’s kept in sync with the code.

Download the raw OpenAPI spec (tenpmuptime-monitor.yaml)

The JSON API a monitor agent uses to enroll with a tenpm uptime server, sync its assigned check set, and report results and status back. This is the wire contract described in agent/model and implemented by internal/server/api_handlers.go; both binaries build against the same Go structs today, so 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. Every route except POST /monitor-api/v1/enroll requires the monitor's own API key, minted at enrollment. POST /monitor-api/v1/enroll requires an enrollment token instead (an org token or a platform token — see the security scheme below), since no monitor exists yet at that point. Error responses across every endpoint are a plain-text body (one line) with a non-2xx status, not a JSON envelope — the handlers use Go's http.Error. The 4xx/5xx responses below document status code and body meaning; there is no shared error schema. Every route here is rate-limited per client address (about 10 requests a second with a burst of 30) and answers 429 rate limit exceeded beyond that. A monitor that syncs and reports every 45 seconds is nowhere near it. Versioning (2026-09-22, path moved again 2026-09-27): every path below moved under /api/v1/ in the first pass, then to /monitor-api/v1/ in a second, unrelated to versioning itself - a new, separate customer-facing account API (/account-api/v1/..., not documented in this file) needed a path of its own and collided with this one's /api/v1/checks and /api/v1/status, so the monitor API got a distinct prefix instead of a third segment under a shared one. The rule from here on: a JSON change within v1 is additive only - a removed field or a changed meaning gets a new /monitor-api/v2/ path instead of changing what v1 means. Neither the fully unversioned form (/api/..., no version segment at all) nor /api/v1/... itself exists any more - both were renamed with no permanent alias, since every monitor deployed at each point in time was ours to redeploy. The /api/v1/... → /monitor-api/v1/... move was briefly served from both prefixes during the fleet's own migration window; that temporary routing was removed once every deployed monitor (this project's own fleet, and any private monitor a customer runs) was confirmed redeployed on a build calling the new path. Treating a prefix rename as a standing policy a third time should not be expected: the next breaking change bumps the version segment (/monitor-api/v2/...), not the prefix.

v3.0.0

Servers#

Local dev server
http://127.0.0.1:8002

Security#

A monitor's own API key, issued once by POST /monitor-api/v1/enroll and persisted by the agent alongside its minted monitor id.
apiKeyAuth
http bearer
An enrollment token minted by an org (from its "Add a monitor" page) or, for the shared fleet, a platform token minted by an admin — the latter expires one hour after minting. Only POST /monitor-api/v1/enroll accepts this token kind; every other endpoint requires a monitor API key instead.
enrollmentTokenAuth
http bearer

API#

Sync the caller's assigned check set#

Returns every check the authenticated monitor should run whose updated_at is strictly after since — including checks that have become deleted: true or been unassigned, so the agent can stop running them, and, for a private-definition check, a row with its definition fields blank (see Check's description).

Store the response's server_time, not the caller's own clock, as the new watermark for the next call's since — using a local clock risks a sync gap under clock skew between the two machines.

Request (requires apiKeyAuth)#

GET /monitor-api/v1/checks

Query parameters#

Parameter nameValueDescriptionAdditional
sincestring (date-time)RFC3339 timestamp; defaults to the Unix epoch (i.e. "everything") when omitted. Must be a server_time value returned by a previous call to this endpoint, not a locally-generated one.

Responses#

200: The delta since since.

Schema: ChecksResponse

PropertyTypeRequiredDescription
checksarray of Checkyes
server_timestring (date-time)yesStore this as the watermark for the next call's since.

400: since was present but not a valid RFC3339 timestamp.

Schema: string

401: Missing/malformed Authorization header, or an unknown API key.

Schema: string

403: The monitor is disabled.

Schema: string

Enroll a monitor#

A monitor's one-time self-registration into the org (or shared fleet) named by the enrollment token it presents. Which of the two the token resolves to is a property of the credential, never of the request body — an agent cannot ask to be shared.

id is optional and means "reclaim the monitor you already know by this id" (used after an enrollment reset); a fresh agent omits it and is minted a new, globally unique id. All other fields are required.

The response's api_key is shown/returned exactly once — it is not retrievable again, only reset (which mints a new one and invalidates the old).

Request (requires enrollmentTokenAuth)#

POST /monitor-api/v1/enroll

Request body (required)#

Schema: EnrollRequest

PropertyTypeRequiredDescription
citystringyes
countrystringyes
idstringnoOmit to enroll as a new monitor. Set to reclaim a previously enrolled monitor by id after an enrollment reset.
namestringyes
regionstringyes

Responses#

200: Enrolled (or reclaimed) successfully.

Schema: EnrollResponse

PropertyTypeRequiredDescription
api_keystringyesShown once. Persist it; it cannot be retrieved again, only reset (which invalidates this value and mints a new one).
monitor_idstringyesMinted by the server (twelve characters of lowercase Crockford base32), globally unique across every org and the shared fleet. Persist this and pass it back as id only to reclaim this same monitor later.

400: Malformed JSON, oversized body, or a required field (name/region/country/city) missing.

Schema: string

401: Missing/malformed Authorization header, or the enrollment token is unknown, revoked, or expired (all three look identical to the caller on purpose).

Schema: string

404: id was set but no monitor with that id exists in the token's organisation. The agent should omit id to enroll as new rather than silently becoming a second node.

Schema: string

409: id was set but that monitor is already enrolled.

Schema: string

Authenticated heartbeat#

A trivial call whose only effect is what the auth middleware already did before the handler ran: updating the monitor's last_seen_at.

Request (requires apiKeyAuth)#

GET /monitor-api/v1/ping

Responses#

200: OK.

Schema: OKResponse

PropertyTypeRequiredDescription
statusstringno

401: Missing/malformed Authorization header, or an unknown API key.

Schema: string

403: The monitor is disabled.

Schema: string

Upload a batch of check execution results#

Every result's monitor_id is stamped from the authenticated caller server-side; whatever the payload contains for it is ignored, so an agent cannot report results as a different monitor. A result for a check the caller is no longer assigned to is silently dropped (not an error) rather than rejected — the rest of the batch still lands, and a stale or racing unassignment must not surface as a client bug.

Insertion is idempotent, so a retried batch (e.g. after a timed-out response whose write actually succeeded) cannot double-count.

A shared monitor's batch may legitimately span several organisations in one call, since one shared agent runs checks assigned by many different orgs; a private monitor's batch belongs entirely to its own org.

Request (requires apiKeyAuth)#

POST /monitor-api/v1/results

Request body (required)#

Schema: ResultsRequest

PropertyTypeRequiredDescription
resultsarray of ResultyesMore than 500 in one call is rejected outright; a monitor with a larger local buffer loops the call instead.

Responses#

200: Results accepted. accepted may be less than the number of results submitted, since unassigned-check results are dropped silently rather than counted or rejected.

Schema: ResultsResponse

PropertyTypeRequiredDescription
acceptedintegeryesMay be less than the number of results submitted, since unassigned-check results are dropped silently rather than counted or rejected.

400: Malformed JSON, or more than 500 results in one batch.

Schema: string

401: Missing/malformed Authorization header, or an unknown API key.

Schema: string

403: The monitor is disabled.

Schema: string

413: Request body exceeded the 1 MiB cap for this endpoint.

Schema: string

Report a monitor's periodic self-health snapshot#

Shown on the server's Monitors page. The monitor_id this updates is the authenticated caller's own id, never taken from the body.

Request (requires apiKeyAuth)#

POST /monitor-api/v1/status

Request body (required)#

Schema: MonitorStatus

PropertyTypeRequiredDescription
last_outage_ended_atstring (date-time)noAbsent until the agent has completed at least one internet- connectivity outage since it started (see monitor.ConnectivityGate). Only completed outages are reported — while offline the agent cannot call this endpoint at all.
last_outage_secintegerno
unsent_resultsintegeryesHow many results are stuck in the agent's local buffer.
uptime_secintegeryes
versionstringyes

Responses#

200: Accepted.

Schema: OKResponse

PropertyTypeRequiredDescription
statusstringno

400: Malformed JSON, or a negative unsent_results/uptime_sec/last_outage_sec.

Schema: string

401: Missing/malformed Authorization header, or an unknown API key.

Schema: string

403: The monitor is disabled.

Schema: string

Models#

Check#

A monitored target, as synced from server to monitor. url's scheme selects the check type: http/https fetch the URL (optional basic-auth userinfo), tcp://host:port tests that a TCP connect succeeds, and tls://host:port performs a bare TLS handshake with no application protocol on top (match_string is then matched against a short certificate summary rather than a banner).

For a private-definition check (one whose definition has been wiped from the server — see private-checks-design.md), the server sends url, match_string, post_data, and headers as empty/absent: the real definition lives only in a JSON file loaded locally into the agent via -import-private-check, out of band from this API.

PropertyTypeRequiredDescription
cert_expiry_warn_daysintegernoFor https/tls checks: fails the check once the presented certificate has this many days or fewer left before it expires, even though the handshake otherwise passed. 0/absent means no such constraint. The boundary itself fails — with 14 set, a certificate with exactly 14 days left fails.
deletedbooleanyesTrue means stop running this check; it is not removed from the delta so the agent learns about the deletion at all.
disable_redirectsbooleannoStop following an http(s) redirect chain; evaluate the first response as-is. Not meaningful for tcp checks.
down_interval_secintegernoServer-side alerting parameter; monitors receive it over sync but ignore it.
enabledbooleanyes
guidstringyesThe only identifier for this check that crosses the wire. The server's internal integer id never does.
headersobjectnoExtra request headers for http(s) checks, at most 10 entries.
insecure_skip_verifybooleannoTurns off TLS certificate verification (curl -k's equivalent). Meaningful for https and tls checks; a no-op for plain http and meaningless for tcp.
interval_secintegeryesHow often the check runs in total across every monitor assigned to it — see monitor_count/monitor_rank.
invert_resultbooleanno"Upside-down mode": flips the final pass/fail verdict, so the check fails when the target is reachable. Applied after every other rule, for every scheme.
match_modestringnoOmitted/absent means contains, so rows predating this field keep their old semantics.
match_stringstringyesEmpty asserts nothing about response content. Otherwise matched against the HTTP body, or (for tcp:// checks) the banner the service volunteers on connect.
max_response_time_msintegernoWhen set, a response arriving after this many milliseconds fails the check even if it otherwise passed. 0/absent means no such constraint.
monitor_countintegernoHow many monitors are currently active on this check (assigned, enabled, and not disabled by the platform). Computed fresh on each sync, never stored. With more than one, the monitors take turns rather than all running the check at once: each runs it every interval_sec × monitor_count, offset by its monitor_rank, so that together they run it once per interval_sec. 0 or 1 means no change from interval_sec on its own.
monitor_rankintegernoThis monitor's own 0-indexed position among the monitor_count active monitors, ordered by monitor id. Determines its offset in the rotation described under monitor_count.
namestringyes
post_datastringnoWhen non-empty, turns an http(s) check into a POST with this body.
resend_interval_secintegernoServer-side alerting parameter (how often a still-failing check repeats its DOWN notification; 0/absent means never); monitors receive it over sync but ignore it.
result_detail_max_charsintegeryesCaps 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_opstringnoWhen set, replaces the built-in "status >= 400 fails" rule with an explicit comparison against status_code_value. Absent means the built-in rule, not "no constraint". Not meaningful for tcp checks.
status_code_valueintegerno
timeout_secintegernoBounds one execution of the check. 0/absent means the default (10s); maximum 60s.
updated_atstring (date-time)yesFixed-width fractional seconds (see model.TimestampLayout) so lexical and chronological order agree; used as the since cursor.
urlstringyes

ChecksResponse#

PropertyTypeRequiredDescription
checksarray of Checkyes
server_timestring (date-time)yesStore this as the watermark for the next call's since.

EnrollRequest#

PropertyTypeRequiredDescription
citystringyes
countrystringyes
idstringnoOmit to enroll as a new monitor. Set to reclaim a previously enrolled monitor by id after an enrollment reset.
namestringyes
regionstringyes

EnrollResponse#

PropertyTypeRequiredDescription
api_keystringyesShown once. Persist it; it cannot be retrieved again, only reset (which invalidates this value and mints a new one).
monitor_idstringyesMinted by the server (twelve characters of lowercase Crockford base32), globally unique across every org and the shared fleet. Persist this and pass it back as id only to reclaim this same monitor later.

MonitorStatus#

A monitor's self-reported health snapshot, sent periodically. Shown on the server's Monitors page.

PropertyTypeRequiredDescription
last_outage_ended_atstring (date-time)noAbsent until the agent has completed at least one internet- connectivity outage since it started (see monitor.ConnectivityGate). Only completed outages are reported — while offline the agent cannot call this endpoint at all.
last_outage_secintegerno
unsent_resultsintegeryesHow many results are stuck in the agent's local buffer.
uptime_secintegeryes
versionstringyes

OKResponse#

PropertyTypeRequiredDescription
statusstringno

Result#

One check execution outcome, as reported from monitor to server.

PropertyTypeRequiredDescription
check_guidstringyesThe check this result is for. A result built on the monitor side must set this and never a server-internal id (there is none to send).
errorstringnoEmpty on success.
http_statusintegeryes
latency_msintegeryes
monitor_idstringyesIgnored on write — the server stamps the authenticated caller's own id regardless of what is sent here.
ran_atstring (date-time)yes
response_samplestringnoLeading characters of what the target sent back (HTTP body, or a tcp check's banner), capped both by the agent before sending and again by the server on ingest at result_detail_max_chars, regardless of what the payload claims.
successbooleanyes

ResultsRequest#

PropertyTypeRequiredDescription
resultsarray of ResultyesMore than 500 in one call is rejected outright; a monitor with a larger local buffer loops the call instead.

ResultsResponse#

PropertyTypeRequiredDescription
acceptedintegeryesMay be less than the number of results submitted, since unassigned-check results are dropped silently rather than counted or rejected.