No matching sections.
Migrate from API v1
If you already have a v1 integration, this page is the map: for each thing you do today, what does the same job in v2. v1 is not going away on a date we have announced - this is a guide for moving when it suits you, not a countdown.
Before you begin
The two APIs are separate surfaces, not two dialects of one. v2 is not a wrapper over v1 and does not call it: different paths, different request and response shapes, its own authentication, its own error format. Moving an integration is a port, not a configuration change - budget for it as such.
Nothing forces a big-bang cutover. The two APIs authenticate independently, so a client can hold a v1 token and a v2 token at once and move endpoint by endpoint - reads first is the usual order, since they are the easy half and prove your token, host and parsing before any write is at stake.
| API v1 | API v2 | |
|---|---|---|
| Base URL | https://api1.host-tracker.com/ (or the proxied www.host-tracker.com/api/web/v1/) | https://api2.host-tracker.com |
| Reference | The v1 reference, one page, eleven languages | The interactive reference generated from the OpenAPI document, plus these guides |
| Credential | A 48-hour ticket exchanged for a login and password, or HTTP Basic | A scoped Bearer token you mint once in the web UI |
Authentication
This is the one change every v1 client has to make, and it is worth making first.
v1 exchanges credentials for a token at runtime: POST users/token
with a login and password returns a ticket valid for 48 hours (HTTP Basic on
every call is accepted too). The token is unscoped - it can do everything the account may do - and
forLogin mints one that acts on behalf of a super-account.
v2 has no credential-exchange endpoint at all. A token is created in the web interface, on the API page of your account, with the scopes it needs and an optional IP allow-list; left alone it lasts ten years. You send it on every call and never handle a password in code again:
curl -X POST 'https://api1.host-tracker.com/users/token' \
-H 'Content-Type: application/json' \
-d '{"login":"[email protected]","password":"…"}'
# then, for the next 48 hours:
curl 'https://api1.host-tracker.com/tasks' \
-H 'Authorization: bearer TICKET'
curl 'https://api2.host-tracker.com/monitor' \
-H 'Authorization: Bearer YOUR_TOKEN'
- A script that logs in at runtime must stop doing that. Mint the token once, keep it where you keep your other secrets, and send it. There is nowhere to POST a password.
- Tokens are scoped, and a read leaf never implies a write leaf. Grant the integration
exactly the actions it performs; a call whose token lacks the scope answers
403 missing_scopenaming what was required and what was granted. See Authentication & scopes. - There is no
forLogintwin. Where a subaccount used impersonation to work inside a super-account, the v2 answer is a token minted by that account with the narrow scopes the integration needs. - The reference tier is anonymous in v2. The agent, pool, IP and type catalogues need no token at all - in v1 most of them required one.
Concept map
Most of the port is renaming. The vocabulary changed once, consistently.
| v1 | v2 | Note |
|---|---|---|
| task | monitor | Same thing, throughout: paths, filters, events. |
| subscription | alert subscription and report subscription | One combined list became two families with their own endpoints. |
info= bitmask | expand= names |
Comma-separated tokens, per endpoint, with a 422 naming the allowed set. |
ticket from users/token |
pre-minted scoped Bearer token | See above. |
agent pool (agentPools) |
pool (locations.pools) |
Same pool ids; a monitor's pools moved into a locations object. |
| outages / incident | span / incident | /span is the raw down-window feed, /incident
the episode feed with comments and rechecks. |
fullLog=true | GET /monitor/incident/{id}/check |
The failing checks inside one episode, still package-gated - see Statistics and results. |
| contact type (4) | contact type (10) |
IM is retired; messenger bots, webhooks and web push are contact types now. |
| ISO strings, or a dual Unix + formatted pair | Unix seconds | Integers only, in both directions. No formatted twin, no ISO, no milliseconds. |
| unpaginated lists | cursor paging | {data, nextCursor, hasMore} on every list. See
The query surface. |
X-HT-Reason + a plain-text body |
application/problem+json (RFC 9457) |
Branch on the stable code member. See Errors. |
| synchronous bulk | job envelopes and delete receipts | A bulk write answers 202 with a jobId you poll.
See Jobs & idempotency. |
| - | new in v2 | Maintenance windows, webhooks, contact groups, status pages, instant checks
(/check), delta sync and generated reports have no v1 twin. |
Tasks to monitors
| v1 | What it did | v2 | What to know |
|---|---|---|---|
GET tasks/types |
15 PascalCase type names | GET /monitor/type |
14 lowerCamel tokens (http, sslExp, …),
anonymous, and each row carries what the old flat list could not:
creatable, attachable,
minInterval, requiresPool,
entitlement, presets.
RusRegBL is gone as a type - it is
type: "http" with settings.preset: "bl:ru". |
GET tasks/intervals |
accepted intervals, in minutes | GET /account → limits.intervals |
Seconds. Per-type floors ride
minInterval on /monitor/type.
domainExp, sslExp,
dnsbl and webRisk run on a fixed cadence
and take no interval at all. |
GET tasks |
the whole list, unpaginated, with info= embeds |
GET /monitor |
Cursor-paged. info= becomes expand=
(see the two sub-tables below). |
GET tasks/{id} |
one task, 200 null when absent |
GET /monitor/{id} |
A missing monitor is 404, not a 200 carrying
null. Clients that tested for null must test the status. |
POST tasks/http · ping ·
port · rusbl |
four typed create paths | POST /monitor |
One door for all 14 types; the type is a body member. Request shape differs - notably
agentPools[] is locations.pools, and an
empty pool list is refused rather than treated as "anywhere" (send
["allworld"]). POST tasks/rusbl was
already dead in v1 (TaskTypeDiscontinued). |
PUT tasks |
update one, or every task a filter matched, synchronously | PATCH /monitor/{id}; in bulk
POST /monitor/bulk-update-validate then
POST /monitor/bulk-update |
The bulk door answers 202 with a job to poll - a mass edit is no
longer one blocking call. Tags are deltas
(addTags/removeTags), not a whole-list
replacement. |
DELETE tasks/{id} and by filter |
delete one, or a filtered set | DELETE /monitor/{id}; in bulk
POST /monitor/bulk-delete-validate then
POST /monitor/bulk-delete |
The single delete answers a receipt listing what went with it. The bulk delete requires the
expectedCount the validate call reported and refuses with
409 selection_mismatch if the selection drifted in between - the
anti-wipe guard v1's "empty filter is refused" rule was reaching for. |
POST tasks/$batch |
a heterogeneous batch of creates, updates and deletes | no successor | Split it into the homogeneous bulk doors above, which are asynchronous and report per-item outcomes. |
The info= bitmask becomes expand=
v1 info | v2 |
|---|---|
subscriptions (1) |
expand=subscription on GET /monitor |
stats (2) |
expand=uptime for the figure on the row, or
GET /monitor/result/summary for the full aggregate |
results (4) |
expand=lastResult for the latest one;
GET /monitor/result (or
GET /monitor/{id}/result) for a window |
attachedResults (8) |
expand=attached for the flags, and
GET /monitor/{id}/attached for the attached checks' own results |
The full /monitor vocabulary is wider than v1's four bits -
settings, attached,
subscription, lastIncident,
maintenance, lastResult (and its
.metrics/.recheck children),
uptime, spans,
summary, count - and an unknown token is refused with
the allowed list rather than ignored.
Filters
| v1 | v2 | Note |
|---|---|---|
ids / excludeIds |
id= |
No exclude form. (includeId= exists but is the opposite: a union that
keeps named rows visible whatever the other filters say.) |
taskType / taskTypes |
type= | lowerCamel tokens; list-accepting. |
url / urls +
urlSearchLike |
url= + like=true |
like without url is refused, never
silently ignored. |
name / names +
nameSearchLike |
q= |
One case-insensitive substring over name and url. |
status=Enabled|Disabled |
enabled=true|false |
enabled is the configured flag. For "not actually being monitored",
which includes an over-limit monitor whose flag is still true, use
state=paused. |
lastState (bool) |
state=up,down,paused,maintenance |
Four states, any combination - a monitor inside a maintenance window is no longer simply "up". |
tags | tag= | Exact match, any-of. |
openStat | openStat= | Unchanged. |
interval / intervals |
no filter | Read interval (seconds) off the rows instead. |
| - | preset=bl:ru, updatedSince=,
sort=, fields= |
New: the Russian-blacklist selector, delta sync, explicit ordering and field projection. |
Contacts
| v1 | What it did | v2 | What to know |
|---|---|---|---|
GET contacts/types |
["Email","IM","SMS","VoiceCall"] |
GET /contact/type |
Anonymous, and ten types: email, sms,
voiceCall, http (a webhook contact),
telegram, viber,
facebook, googleChat,
discord, webPush.
IM is retired. Each row also carries its gateways,
whether it can carry reports, and its accepted alert delays. |
GET contacts/delays |
the accepted alertDelay values |
folded into GET /alert/type
(summary.alertDelays), GET /contact/type
and GET /account (limits.alertDelays) |
Still minutes - the one duration on the surface that is not seconds, because it
is a ladder of allowed values rather than a free number. Same ten values, now sorted:
[0,3,5,15,30,60,180,360,720,1440]. An off-ladder value is
422 invalid_alert_delay with allowed[]. |
GET contacts/sms/gateways,
contacts/im/gateways |
gateway ids | the gateways[] member of each /contact/type row |
IM has no successor. |
GET contacts, contacts/{id} |
list and read, with info=subscriptions |
GET /contact, GET /contact/{id} |
Cursor-paged; expand=subscription|template|group|summary|count. |
POST contacts/email · sms ·
voice |
three typed create paths | POST /contact |
One door, type in the body. A duplicate address binds to the existing contact and
answers 200 instead of v1's SimilarExists refusal; an email
whose domain has no MX is refused with undeliverable_domain. |
PUT contacts |
update by id or filter | PATCH /contact/{id}; in bulk POST /contact/bulk |
The bulk door is a job and takes mixed creates, updates and deletes. An address collision on
update is 409 duplicate_contact. Watch one semantic change:
activePeriod.days: [] means paused, not "every day". |
DELETE contacts/{id} and by filter |
delete one or a filtered set | DELETE /contact/{id}; in bulk
POST /contact/bulk-delete-validate then
POST /contact/bulk-delete |
Same two-phase expectedCount guard as monitors. |
PATCH contacts/{id}/code |
resend the confirmation code | POST /contact/{id}/confirmation |
Rate-limited per contact and per account; a refusal is 429 with
Retry-After. |
POST contacts/{id}/code |
confirm with the code | POST /contact/{id}/confirmation/verify |
Same body: the code the contact received. |
POST contacts/$batch | synchronous batch | POST /contact/bulk | Asynchronous job. |
Alerts and subscriptions
v1 had one subscription list carrying both alert types and report types. v2 has two families with their own endpoints, because they answer different questions and have different rules - reports go only to email contacts, and their frequency is a package matter.
| v1 | What it did | v2 | What to know |
|---|---|---|---|
GET subscriptions/alertTypes |
Up, Down, RepeatedlyDown |
GET /alert/type |
lowerCamel: up, down,
repeatedlyDown. The alert-delay ladder rides its
summary. |
GET subscriptions/reportTypes |
Day … Year |
GET /report/type |
The report catalogue: each row's frequencies[] holds
daily, weekly,
monthly, quarterly,
yearly, beside its formats and sections. |
GET subscriptions |
one flat list of alert and report subscriptions | GET /alert and GET /report |
Two calls, merged client-side if you really want one list. Each family also has grouped views
(/alert/by-monitor, /alert/by-contact,
and the report twins) and nested reads
(GET /monitor/{id}/alert,
GET /contact/{id}/alert, …). Filters are prefixed by side:
monitor.type=, contact.confirmed=. |
POST / DELETE subscriptions |
cross-join add/remove, 204; an omitted side meant "all" |
POST /alert/bulk and POST /report/bulk |
One door per family, taking
{create[], delete[], allMonitors?, allContacts?} - so a gesture that
touched both families is two calls and no longer one transaction. The wildcard is now explicit
rather than an omitted member. A selection is capped at 2 000 enumerated rows (a wildcard side
does not count); create and delete
naming the same pair is refused; an explicitly named non-email contact on a report is
422 unsupported_report_channel; a frequency your package does not
include is 403 package_limit. |
Statistics and results
| v1 | What it did | v2 | What to know |
|---|---|---|---|
GET stats |
uptime aggregates | GET /monitor/result/summary |
One endpoint for the account roll-up (groupBy=account) and the
per-monitor and per-bucket breakdowns. Read
uptimePercent rather than recomputing it from the seconds
beside it - it is the figure every other surface reports. |
| - | - | the same endpoint's sla= parameter |
New in v2: score a window against a target. The three members are
slaTarget, slaMet and
errorBudgetSecRemaining (seconds of downtime still affordable;
negative means overspent). All three are omitted when no target applies - a monitor's own
slaTarget, or sla= on the request. |
GET outages |
down windows with event numbers and comments | GET /monitor/{id}/span, or
GET /monitor/incident for the account-wide feed |
Spans are the raw down windows; incidents are the episodes, with severity, state, duration and
comments (POST /monitor/incident/{id}/comment). |
GET incident |
failure events | GET /monitor/incident,
GET /monitor/{id}/incident,
GET /monitor/incident/{id} |
Cursor-paged, filterable by state,
severity and the usual monitor filters.
expand=recheck adds the recheck constellation - which locations
confirmed the failure and which disagreed - and needs no entitlement. |
GET incident?fullLog=true |
every failing check of an outage | GET /monitor/incident/{id}/check |
The same capability, its own endpoint, and the same package entitlement: an
account without it is refused with 403 package_limit naming
apiFullLog, where v1 answered
ApiFullLogNotAllowed. Rows are result rows, so each links straight to
its result and snapshot; expand=metrics decodes the measurements and
the assertions that failed. |
GET incident/snapshot |
the stored response body, addressed by task + check + timestamp | GET /monitor/{monitorId}/result/{id}/snapshot |
Addressed by the result's own id, so resolve the result first - from the incident's check list or
GET /monitor/{id}/result. |
GET results/http |
averaged http timings | GET /monitor/result/summary?metrics=responseTime,dns,connect,tls,ttfb,transfer |
Bucketed series of typed points {t, value, p95, samples}.
headTime and dataTime are now
ttfb and transfer;
tls is new. |
GET cr/httpResponseTime |
a raw [unix, ms] series |
metrics=responseTime on the summary, or
GET /monitor/{id}/result for the raw checks |
There are no two-element tuples on the v2 wire; every point is a named object. |
groupBy=account covers up to 30 days and 2 000 monitors). See
Results, uptime & SLA.
Agents
| v1 | What it did | v2 | What to know |
|---|---|---|---|
GET agents | the agent list, authenticated | GET /agent |
Anonymous in v2, and each row carries more:
ip, ipv6,
visible. The v1 touched parameter has no
twin. |
GET agents/pools | the pool list, authenticated | GET /agent/pool |
Anonymous too, but auth-aware: a token adds the account's hidden pools and its limits. This
endpoint accepts no query parameters at all - even
limit is 422 unknown_parameter. |
GET agents/ips |
the whitelist as text/plain, or JSON on
Accept |
GET /agent/ip |
JSON only. There is no text/plain mode - Accept:
text/plain still answers application/json - and the rows are
objects, not bare strings:
{ip, family, country, addedAt}. A firewall script that piped the
plain list into a rule now needs to pluck .data[].ip. |
curl -s 'https://api2.host-tracker.com/agent/ip' | jq -r '.data[].ip'
Conventions that changed
| Convention | v1 | v2 |
|---|---|---|
| Dates | ISO-8601 UTC strings on tasks; a dual pair on statistics - a
…Unix integer beside a string formatted to the account's profile |
Unix seconds, one integer, everywhere, in both directions. Formatting is the client's job. |
| Errors | 4xx with the machine code in the
X-HT-Reason header and prose in the body |
application/problem+json with
code, title,
detail and an errors[] array pointing at
the offending member. Branch on code. |
| Unknown input | ignored | refused - 422 unknown_parameter /
422 validation_failed, naming what is accepted. A typo can no
longer silently leave a default in place. |
| Paging | none - a list was the whole list | {data, nextCursor, hasMore}; pass
nextCursor back as cursor. Never build a
cursor yourself. |
| Rate limiting | 6 requests/second and 120/minute per account and endpoint;
429 RequestThrottled |
Per-scope quota with RateLimit-Limit,
RateLimit-Remaining and
RateLimit-Reset on the answer,
429 quota_exceeded plus Retry-After when
spent, and GET /account/quota to check headroom without spending
any. |
| Request tracing | - | Every response carries x-request-id. Log it; it is what support needs
to find your call. |
| XML | Accept: application/xml returned an XML body |
JSON only. An XML Accept header is answered with
JSON. |
| Method override | X-HTTP-Method-Override let a POST stand in for PUT/DELETE/PATCH |
Not supported. Send the real verb. |
| Retries | - | Idempotency-Key is honoured on writes and required on the
destructive ones. See Jobs & idempotency. |
What has no successor
POST users/token- v2 mints tokens in the UI, never from credentials, and there is noforLoginimpersonation twin.- HTTP Basic - the Bearer token is the only scheme.
POST tasks/$batchandPOST contacts/$batch- use the per-family bulk doors, which are asynchronous jobs with per-item outcomes.IMcontacts and their gateways - retired. The messenger channels are their own contact types now.- XML responses and
X-HTTP-Method-Override. - The
intervalfilter on the monitor list, and theexclude*inversions of the id, type, url and name filters.
Where to go next
- Quickstart - mint the token and make the first call.
- The query surface - cursors, filters,
expandandfields, once for every list. - Monitors - the per-type settings a ported create needs.
- Errors - what to branch on now that
X-HT-Reasonis gone. - The v1 reference - still here, still accurate for the API you are leaving.