Skip to content

REST API v2

View as Markdown

The REST API v2 is the same API the HostTracker app is built on: 182 operations across monitors, results and incidents, maintenance windows, contacts and subscriptions, reports, status pages, webhooks, jobs, account data and instant checks. This page is the working rulebook - enough to build correct requests without guessing. The endpoint-by-endpoint reference with every schema is the interactive API reference.

Base URL https://api2.host-tracker.com - routes sit at the root (/monitor, /contact, …), with no version prefix
Auth Authorization: Bearer <token>, token minted at Integrations -> API (/integrations/api). See API authentication
Plan API access is a plan feature - see which plans include the API
Format JSON in and out (Content-Type: application/json)
Time Every timestamp is Unix seconds (UTC integers), in both directions. Durations are seconds unless a field says otherwise
Ids Opaque strings (most are GUIDs). Never build one yourself
Errors RFC 9457 application/problem+json with a stable code - see Errors
OpenAPI document GET https://api2.host-tracker.com/openapi/v2.json (OpenAPI 3.1, anonymous)
Error code pages https://api2.host-tracker.com/problems/{code}
Terminal window
export HT_TOKEN="your-api-token"
# 1. Who am I, what may I do? (scope account:read)
curl https://api2.host-tracker.com/account -H "Authorization: Bearer $HT_TOKEN"
# 2. My monitors that are down (scope monitor:read)
curl "https://api2.host-tracker.com/monitor?state=down&limit=20" -H "Authorization: Bearer $HT_TOKEN"
# 3. Where checks can run from (no token needed)
curl https://api2.host-tracker.com/agent/pool

A create example - an HTTP monitor that looks for a keyword and texts a new SMS contact when it goes down or recovers (scopes monitor:write and contact:write):

Terminal window
curl -X POST https://api2.host-tracker.com/monitor \
-H "Authorization: Bearer $HT_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-shop-monitor-1" \
-d '{
"type": "http",
"url": "https://shop.example.com/",
"name": "Shop home page",
"interval": 60,
"locations": { "pools": ["allworld"] },
"settings": { "keywords": "Add to cart" },
"contacts": [ { "ref": "oncall", "type": "sms", "address": "+15551234567" } ],
"alertSubscriptions": [ { "contactRefs": ["oncall"], "alertTypes": ["down", "up"] } ]
}'

interval is in seconds and must be one your plan allows (GET /account lists them under limits.intervals). locations.pools is required for location-based types - ["allworld"] means everywhere (GET /agent/pool lists the pools). The new SMS contact is created unconfirmed and receives a confirmation code; it gets alerts once confirmed. Monitor fields per type: monitor settings reference.

Area Main operations Scope
Monitors GET/POST /monitor, GET/PATCH/DELETE /monitor/{id}, POST /monitor/{id}/copy, POST /monitor/bulk, /bulk-update, /bulk-delete (+ -validate twins), POST /monitor/{id}/reset-stats, POST /monitor/validate-cron, POST /monitor/resolve monitor:read / monitor:write
Monitor types GET /monitor/type, GET /monitor/type/{type}, GET /monitor/type/schema none
Results GET /monitor/result, GET /monitor/{id}/result, GET /monitor/{id}/result/{resultId}, .../snapshot, GET /monitor/result/summary, GET /monitor/{id}/span, GET /monitor/{id}/attached monitor:read
Incidents GET /monitor/incident, GET /monitor/{id}/incident, GET /monitor/incident/{id}, GET /monitor/incident/{id}/check, POST /monitor/incident/{id}/comment monitor:read (comment: monitor:write)
Maintenance GET/POST /maintenance, GET/PATCH/DELETE /maintenance/{id}, GET /monitor/{id}/maintenance monitor:read / monitor:write
Reports POST /monitor/report (job), GET /monitor/report/{id}, GET /monitor/report/{id}/content; GET /report/type monitor:write to generate, monitor:read to fetch
Contacts GET/POST /contact, GET/PATCH/DELETE /contact/{id}, confirmation, test, bulk; GET /contact/type contact:read / contact:write
Contact groups GET/POST /contact/group, GET/PATCH/DELETE /contact/group/{id}, PUT /contact/{id}/group contact:read / contact:write
Notification log GET /contact/notification, GET /contact/{id}/notification, GET /contact/notification/summary, POST /contact/notification/resend contact:read (resend: contact:write)
Alert subscriptions GET/PUT/DELETE /monitor/{id}/alert/{contactId} (and the mirror under /contact/{id}/alert/{monitorId}), GET /alert, POST /alert/bulk; GET /alert/type monitor:* or contact:* by side; flat lists subs:read; bulk needs both writes
Report subscriptions Same shape under /report and /monitor/{id}/report/{contactId} as above
Webhooks GET/POST /webhook, GET/PATCH/DELETE /webhook/{id}, POST /webhook/{id}/test, GET /webhook/{id}/delivery, .../redeliver webhook:read / webhook:write
Status pages GET/POST /statuspage, GET/PATCH/DELETE /statuspage/{id}, PUT /statuspage/{id}/component, incidents, timeline, templates, subscribers statuspage:read / statuspage:write
Jobs GET /job, GET /job/{id}, POST /job/{id}/cancel, POST /job/{id}/resume the scope of the operation that created the job
Account GET/PATCH /account, GET /account/quota, GET /account/usage, GET /account/member account:read / account:write
Instant checks POST /check, GET /check, GET /check/{dbId}/{id}; GET /check/type, GET /check/device check:write / check:read
Locations GET /agent, GET /agent/pool, GET /agent/ip none

Every paged list also has a POST <path>/q twin that takes the same parameters as a JSON body (see Body queries).

  • Create answers 201 Created with the full resource and a Location header. There is no need to read it back.
  • Update (PATCH) changes only the members you send. An absent member is left alone; an explicit null clears a nullable member.
  • Delete answers 200 with a receipt naming what was removed, including anything that went with it (for example a monitor’s subscriptions).
  • Unknown members are refused, never ignored: a misspelled field answers 422 validation_failed with reason unknown_member and the list of accepted members.
  • A monitor is unique per (url, type). Creating a second one answers 409 duplicate_monitor with the existing monitor’s id.
  • A monitor’s type cannot change after creation (422 type_immutable).

Every list answers the same envelope:

{
"data": [ { "id": "..." } ],
"nextCursor": "eyJrIjoi...",
"hasMore": true
}
  • limit is 1-500, default 50. An out-of-range value is refused with 422 invalid_limit, never clamped.
  • To get the next page, send nextCursor back as cursor. Stop when hasMore is false (nextCursor is then null). There is no page number or offset anywhere.
  • Cursors are opaque and checksummed. An edited or foreign cursor answers 422 invalid_cursor. A cursor is bound to the sort it was issued under.
  • Some lists add count ({total, matched}) when you ask with expand=count, and a summary block with expand=summary.

The query string is a closed vocabulary: each endpoint refuses what it does not define.

  • An unknown parameter answers 422 unknown_parameter with the full allowed list (names are case-sensitive: updatedsince is refused, updatedSince works).
  • An unknown value answers 422 unknown_enum_value with the allowed values.
  • A parameter sent empty (?type=) is refused, never read as “no filter”.
  • A list-valued filter is ANY-OF (type=http,ping or type=http&type=ping); different parameters combine with AND.

Example: GET /monitor?type=http,ping&state=down&tag=prod&sort=name.

One parameter: sort=<column> or sort=<column>:asc|desc. Unsuffixed, time columns read newest first and name columns A to Z. There is no order= parameter.

List Sort columns updatedSince expand=count
GET /monitor name, state, type, interval, lastChange, url, created (default created) yes yes
GET /contact created, name, address yes yes
GET /maintenance from (default), created yes -
GET /webhook created, updated, name, url yes -
GET /monitor/result, GET /monitor/incident time (default), monitor - yes
GET /statuspage created, title, slug - -
GET /contact/group name, created - -
  • expand=a,b adds blocks to each row. Lists default to bare rows; a single read defaults to the object’s own detail (for GET /monitor/{id}, settings). Sending expand replaces the defaults. An unknown token answers 422 unknown_expand with the allowed tokens.
  • Monitor tokens: settings, attached, subscription, lastIncident, lastResult, maintenance, uptime, spans, summary, count.
  • On rows derived from a monitor (results, incidents, maintenance windows), expand=monitor embeds the monitor, and monitor.settings, monitor.subscription, monitor.lastIncident, monitor.maintenance embed its blocks.
  • fields=id,name,state keeps only those top-level members of each row (id is always kept). An unknown name answers 422 unknown_field.

Every paged list also answers at POST <path>/q with the same parameters as a JSON object - arrays for list filters. Use it when a query string would be too long (hundreds of ids) or contains awkward characters. Cursors work across both forms.

Terminal window
curl -X POST https://api2.host-tracker.com/monitor/q \
-H "Authorization: Bearer $HT_TOKEN" -H "Content-Type: application/json" \
-d '{ "type": ["http","ping"], "state": ["up"], "sort": "name", "limit": 100 }'

GET /monitor, GET /contact, GET /maintenance and GET /webhook also return a syncCursor. Send it back as updatedSince to get only what changed. Be aware of what each one catches:

  • Maintenance windows and webhooks: every edit.
  • Monitors: creation, up/down changes and automatic plan-limit disabling only - not renames, setting changes, tag edits or manual pause/resume.
  • Contacts: creation only.
  • None of them report deletions.

To hear about the rest, subscribe a webhook to monitor.updated, monitor.deleted and contact.updated, and reconcile with a full list from time to time.

from and to (Unix seconds) select a time slice. Some endpoints cap the width and refuse a wider window with 422 invalid_range, reporting maxSpan in seconds: raw results allow at most 30 days per request (and 24 hours when no monitor is named). Incident lists and uptime summaries are not capped the same way. Details: Reading results.

Every failure, on every endpoint, is one shape:

{
"type": "https://api2.host-tracker.com/problems/invalid-interval",
"title": "The requested check interval is not allowed for this account.",
"status": 422,
"code": "invalid_interval",
"errors": [ { "pointer": "/interval", "value": 300, "allowed": [60] } ]
}
  • Branch on code, never on title or detail (those may be reworded).
  • errors[] says what to fix: pointer (a JSON Pointer into your body, or /<name> for a query parameter), value (what you sent), allowed, reason, expected, didYouMean, and per-code members. An absent member means “no information”, not null.
  • Every response carries an x-request-id header; a 500 internal_error repeats it as traceId. Quote it to support.
  • Failures inside an async job use the same shape in results[].error.

Three families of fix:

Family Statuses What to do
Fix the request 400, 405, 413, 415, 422 Read errors[], change the payload, resend. Never retry blindly.
Fix the account state 401, 402, 403, 404, 409 A token, scope, plan limit, allow-list or conflicting resource must change first.
Wait and retry 429, 500, 502, 503 Honour Retry-After; otherwise back off with jitter.

The full list of codes: Error codes reference.

Send Idempotency-Key: <opaque string, max 255 chars> on a write, and a retry after a timeout is safe: the repeat returns the first call’s stored response instead of doing the work again.

  • Keys are scoped to your account and remembered for 24 hours. Use one key per logical operation (a UUID is ideal) and reuse it only for that operation’s retries.
  • Same key, same body: the stored response, byte for byte, with the header Idempotency-Replayed: true.
  • Same key, different body: 409 idempotency_key_conflict, reason different_body.
  • Same key while the first call still runs: 409 idempotency_key_conflict, reason in_flight, with Retry-After.

Required (without it: 400 idempotency_key_required):

Operation Why
POST /monitor/bulk, /monitor/bulk-update, /monitor/bulk-delete, POST /monitor/{id}/reset-stats, POST /contact/bulk, /contact/bulk-delete, POST /monitor/report They answer before the work is done, so a timed-out request cannot tell you whether it landed.
POST /statuspage/{id}/incident, POST /statuspage/{id}/incident/{incidentId}/timeline They notify subscribers; a keyless retry announces twice.
POST /job/{id}/cancel, POST /job/{id}/resume Job control.
POST /monitor with inline contacts, POST /contact for a type that gets a confirmation code (email, sms, voiceCall), POST /monitor/{id}/copy for more than 10 addresses A retry would resend paid confirmation codes or copy twice.

Every other write accepts the header. Sending one on every write is always safe - the SDKs and ht-cli do it for you.

Bulk operations, report generation, large copies and statistics resets answer 202 Accepted with a job:

HTTP/2 202
Location: /job/be65c73e-4e1a-49d7-8ef9-ba9b11c8681c
Retry-After: 3
{ "jobId": "be65c73e-4e1a-49d7-8ef9-ba9b11c8681c", "accepted": 2 }

Poll GET /job/{id} after the Retry-After seconds. The poll always answers 200; read state:

State Terminal Meaning
queued no Accepted, not started.
running no In progress; progress.done / progress.total advance.
succeeded yes Every item succeeded.
partial yes Some items succeeded, some failed - retry only the failed items.
failed yes Every item failed.
cancelled yes Cancellation took effect.
interrupted no The server running it stopped; continue it with POST /job/{id}/resume.
  • results[] has one row per item: index (position in your request), itemRef, status (pending, created, createdDisabled, updated, skipped, deleted, failed, cancelled), entityId, result and, for a failed item, error (a full problem document).
  • A non-terminal poll carries a fresh Retry-After; a terminal one carries none.
  • Jobs stay readable for 7 days (expiresAt), then answer 404. GET /job lists recent jobs.
  • Instead of polling, add "callback": {"webhookId": "..."} to the job-creating body to receive a job.completed webhook (add "on": "progress" for interim job.progress deliveries too).
  • POST /job/{id}/cancel stops future items; finished items are not undone. A finished job answers 409 job_not_cancellable.

Destructive bulk deletes are two-phase. Call POST /monitor/bulk-delete-validate with a filter to see matched and a sample, then POST /monitor/bulk-delete with the same filter and "expectedCount" set to matched. If the selection changed in between, the API answers 409 selection_mismatch and deletes nothing. The same pair exists for contacts.

Catalogues that describe the service rather than your account need no token: monitor, contact, alert, report and instant-check types, device profiles, the settings schema, and the locations (/agent, /agent/pool, /agent/ip). They have their own per-address rate limit. Sending a token is allowed: GET /monitor/type then adds your plan’s limits per type, and GET /agent/pool adds your saved presets.