HostTracker.Sdk 0.1.0

dotnet add package HostTracker.Sdk --version 0.1.0
                    
NuGet\Install-Package HostTracker.Sdk -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="HostTracker.Sdk" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="HostTracker.Sdk" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="HostTracker.Sdk" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add HostTracker.Sdk --version 0.1.0
                    
#r "nuget: HostTracker.Sdk, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package HostTracker.Sdk@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=HostTracker.Sdk&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=HostTracker.Sdk&version=0.1.0
                    
Install as a Cake Tool

HostTracker .NET SDK

The official .NET client for the HostTracker API v2 - uptime, blacklist, certificate and instant checks, contacts and alerting, webhooks, status pages and reports.

  • Package: HostTracker.Sdk · Target: net8.0
  • API reference: https://www.host-tracker.com/apidocs/v2 · Base URL: https://api2.host-tracker.com
  • The typed surface is generated from the published OpenAPI document; a hand-written layer adds auth, retry, idempotency, paging, job/check polling and webhook verification.
dotnet add package HostTracker.Sdk

Quick start

Mint a token on your profile page (scopes: monitor, check, webhook). It is a long-lived JWT - keep it out of source control.

using HostTracker.Sdk;
using HostTracker.Sdk.Generated;

using var client = new HostTrackerClient(Environment.GetEnvironmentVariable("HT_TOKEN"));

// 1. List monitors.
var page = await client.Monitors.ListMonitorAsync(limit: 50);
foreach (var m in page.Data)
    Console.WriteLine($"{m.Name,-30} {m.State} since {UnixTime.ToDateTimeOffset(m.Since):u}");

// 2. Create one.
var created = await client.Monitors.CreateMonitorAsync(new MonitorWriteRequest
{
    Type = MonitorTypes.Http,
    Url = "https://example.com",
    Name = "Marketing site",
    Interval = 5,
    Locations = new MonitorLocations { Pools = new[] { "allworld" } },
});
Console.WriteLine($"created {created.Id}");

// 3. Run an on-demand check and wait for the verdict.
var result = await client.RunCheckAsync(new IcCreateRequest
{
    Url = "https://example.com",
    Type = "http",
});
Console.WriteLine($"{result.State} with {result.Events?.Count ?? 0} location report(s)");

// 4. Verify a webhook delivery (in your endpoint, on the RAW bytes).
var verdict = WebhookSignature.Verify(request.Headers, rawBody, secrets: new[] { webhookSecret });
verdict.EnsureValid();
var evt = WebhookEvent.Parse(rawBody);
if (evt.Event == WebhookEvents.MonitorDown)
    Console.WriteLine(evt.DataAs<WebhookMonitorAlert>()!.Monitor!.Url);

Configuration

using var client = new HostTrackerClient(new HostTrackerOptions
{
    Token = "…",                                   // omit for the anonymous reference tier
    BaseUrl = "https://api2.host-tracker.com",     // default
    Timeout = TimeSpan.FromSeconds(30),            // PER ATTEMPT, not per call
    MaxRetries = 2,
    MaxRetryDelay = TimeSpan.FromSeconds(60),
    Idempotency = IdempotencyMode.Auto,
    UserAgentSuffix = "acme-deploy/2.1",
    Handler = new HttpClientHandler { Proxy = myProxy },
});
Option Default What it does
Token - Authorization: Bearer … on every request. Omit it and only the anonymous reference tier answers.
BaseUrl https://api2.host-tracker.com A path prefix is honoured (…/api2/).
Timeout 30 s Budget for one HTTP attempt. A call honouring two Retry-After waits is not killed by it.
MaxRetries 2 Retries after the first attempt.
MaxRetryDelay 60 s Ceiling on a single honoured Retry-After.
Idempotency Auto Stamps a fresh Idempotency-Key on every write.
UserAgentSuffix - Appended to hosttracker-sdk-dotnet/<version>.
Handler HttpClientHandler The innermost transport - proxies, TLS, DNS.
HttpClient - Total control: the SDK adds no handlers of its own to it.

The client is thread-safe and long-lived - build one per process (or register it as a singleton), not one per call.

Operations

Operations are grouped by their API family; each method is named after its operationId:

Monitors, MonitorTypes, Results, Incidents, Maintenance, Contacts, Alerts, Reports, Webhooks, StatusPages, Account, MonitoringLocations, InstantChecks (alias Checks), Jobs.

await client.Monitors.ListMonitorAsync(state: new[] { MonitorStates.Down }, expand: new[] { "lastIncident" });
await client.Contacts.CreateContactAsync(new ContactWriteRequest { Type = ContactTypes.Email, Address = "ops@example.com" });
await client.Webhooks.TestWebhookAsync(webhookId);

Every paged GET also answers a POST <path>/q body-query twin with the same parameters as one JSON object - reach for QueryMonitorAsync(new MonitorQueryRequest { … }) when a filter is too long or too awkward for a URL. The answer is identical.

Two operations return binary content as a FileResponse (a stream plus its headers): GetMonitorResultSnapshotAsync (the check's screenshot) and GetReportContentAsync.

Errors

Every failure - a problem document, an HTML 502 from a proxy, a DNS failure, a timeout - arrives as a single HostTrackerException. Branch on Code, never on StatusCode alone: rate_limited and quota_exceeded are both 429 and want opposite handling.

try
{
    await client.Monitors.CreateMonitorAsync(request);
}
catch (HostTrackerException ex) when (ex.IsCode(ProblemCodes.DuplicateMonitor))
{
    var existing = ex.Errors.FirstOrDefault()?.Extensions["existingId"].GetString();
}
catch (HostTrackerException ex) when (ex.IsCode(ProblemCodes.QuotaExceeded))
{
    // The allowance is spent - wait for the reset or upgrade. Retrying changes nothing.
}
catch (HostTrackerException ex)
{
    Console.Error.WriteLine($"{ex.StatusCode} {ex.Code}: {ex.Detail} (request {ex.RequestId})");
    foreach (var e in ex.Errors) Console.Error.WriteLine($"  {e.Pointer}: {e.Reason}");
}

Status, Code, Type, Title, Detail, Instance, Errors[], RequestId, RetryAfter, RateLimit and the raw Response are all on the exception. Codes the SDK predates pass through as plain strings - ProblemCodes names the ones worth branching on, not all of them.

Retries and idempotency

Retried automatically: 429 rate_limited (honouring Retry-After, capped at MaxRetryDelay), 503 service_unavailable when it carries a Retry-After, a 429/503 with no problem body at all (an edge throttle in front of the API can answer in plain text), and transport failures. Never quota_exceeded. Without a Retry-After the wait is full jitter over 200ms · 2^n, capped at 5 s.

A write is retried only when an Idempotency-Key rides with it, which in the default Auto mode is always: the SDK stamps a fresh UUID on every POST/PATCH/PUT/DELETE (never on a /q twin - that is a read), and the SAME key is reused across the retry, so the server replays its stored answer instead of executing twice. Some operations require the header; the auto key satisfies them, and passing one explicitly always wins:

await client.Monitors.BulkCreateMonitorAsync(request, idempotency_Key: myOwnKey);

Response metadata

The generated methods return the body. Open a capture scope for what rode alongside it:

using var capture = client.CaptureResponses();
var page = await client.Monitors.ListMonitorAsync(limit: 50);

var meta = capture.Last!;
Console.WriteLine($"{meta.RequestId} attempts={meta.Attempts} replayed={meta.IdempotencyReplayed}");
Console.WriteLine(meta.RateLimit);   // policy=account;q=1000;w=60 limit=1000 remaining=997 reset=42

RateLimit.Policy may be the literal none (Unmetered), in which case the numeric members are absent rather than zero.

Paging

Lists are cursor-paginated: { data, nextCursor, hasMore }. Cursors are opaque - never build, parse or reorder one, and never change the sort mid-walk.

await foreach (var monitor in Pagination.PaginateAsync<MonitorPage, MonitorView>(
    (cursor, ct) => client.Monitors.ListMonitorAsync(limit: 200, cursor: cursor, cancellationToken: ct)))
{
    Console.WriteLine(monitor.Url);
}

PagesAsync yields the envelopes instead, keeping SyncCursor, Count and Summary reachable. Every *Page type also implements IPageEnvelope<T> if you would rather walk it yourself.

Jobs

Bulk mutations answer 202 { jobId, accepted } and finish asynchronously.

var accepted = await client.Monitors.BulkCreateMonitorAsync(request);
var job = await client.WaitForJobAsync(accepted.JobId);

if (JobStateInfo.IsPartial(job.State))          // partial is a SUCCESS with some failed items
{
    await foreach (var item in client.JobResultsAsync(accepted.JobId))
        if (item.Status == JobItemStatuses.Failed) Console.WriteLine($"row {item.Index}: {item.Error}");
}

WaitForJobAsync paces itself with the Retry-After on every non-terminal poll and returns on succeeded, partial, failed and cancelled. It also returns an interrupted job - which is not terminal: the server running it died, and only you can decide whether to client.Jobs.ResumeJobAsync(jobId).

Instant checks

var result = await client.RunCheckAsync(
    new IcCreateRequest { Url = "https://example.com", Type = "http", Pools = new[] { "allworld" } },
    new RunCheckOptions { OnPoll = r => Console.WriteLine($"{r.Events?.Count ?? 0} reports so far") });

RunCheckAsync posts the check and then follows the resultUrl the server handed back until state == "done", honouring each poll's retryAfter. A check is addressed by the pair (dbId, id) - never build that path yourself. The SDK follows resultUrl on the configured host only. When the checking pipeline is unavailable the create is refused with 503 service_unavailable rather than returning an id that never resolves.

Webhooks

Every delivery carries both HostTracker's own HT-Signature and the Standard Webhooks triple. Verify the raw request bytes before parsing - a re-serialized body no longer matches its signature.

app.MapPost("/hooks/hosttracker", async (HttpRequest req) =>
{
    using var buffer = new MemoryStream();
    await req.Body.CopyToAsync(buffer);
    var rawBody = buffer.ToArray();

    var verdict = WebhookSignature.Verify(
        req.Headers.Select(h => new KeyValuePair<string, IEnumerable<string>>(h.Key, h.Value!)),
        rawBody,
        secrets: new[] { currentSecret, previousSecret });   // both, while a rotation is in flight
    if (!verdict.IsValid) return Results.Unauthorized();

    var evt = WebhookEvent.Parse(rawBody);
    if (!seen.Add(evt.Id)) return Results.Ok();              // HT-Delivery is stable across retries
    …
    return Results.Ok();
});

Rotation (PATCH /webhook/{id} {"secret":{"rotate":true}}) signs with both secrets for 24 hours and puts two v1 values in the header - pass both secrets and either match verifies. The tolerance is 300 seconds by default (tolerance:), and now: makes a test deterministic. WebhookScheme.StandardWebhooks forces the other scheme; the default Auto prefers HT-Signature when it is present.

Timestamps

Unix seconds in both directions, everywhere, including inside webhook envelopes - never ISO-8601, never milliseconds. The wire types keep the integers:

DateTimeOffset since = UnixTime.ToDateTimeOffset(monitor.Since);
long from = UnixTime.FromDateTimeOffset(DateTimeOffset.UtcNow.AddDays(-7));

(Members whose name ends in Ms - a delivery's latencyMs - are elapsed milliseconds, not instants.)

Staying forward compatible

The API adds endpoints, response members and vocabulary values without a version bump, so the SDK is built not to break on them: unknown response members are kept in each type's AdditionalProperties, every vocabulary is a plain string (with MonitorTypes, MonitorStates, JobStates, WebhookEvents, … as constants for discoverability), and unknown problem codes pass through. A value the server adds to an open vocabulary arrives as itself rather than as a deserialization failure.

For anything the typed surface cannot express yet - an explicit null in a PATCH body ("clear this", as opposed to an absent member's "leave alone"), or an endpoint newer than your SDK build - there is one escape hatch that still rides the full pipeline:

await client.SendJsonAsync(HttpMethod.Patch, "/account",
    new Dictionary<string, object?> { ["defaultAgentPools"] = null });

The path may be rooted (/account), relative (account) or an absolute http(s) URL; a rooted path keeps a base-URL path prefix. Anything that would resolve to another scheme is refused rather than dialled - including a resultUrl the server hands back.

Regenerating the client

src/HostTracker.Sdk/Generated/ is generated by NSwag (pinned in .config/dotnet-tools.json) and committed - consumers never run the generator. Never hand-edit it; everything custom lives beside it in src/HostTracker.Sdk/.

./scripts/regen.sh                                    # from the public openapi repo
HT_SPEC=/path/to/openapi-3.0.json ./scripts/regen.sh  # from a local document

scripts/prep-spec.py normalizes the document first (vocabularies, oneOf unions, format: uri, the Idempotency-Key header); the transforms and why each one exists are listed at the top of that file.

Development

dotnet build HostTracker.Sdk.sln -c Release
dotnet test  HostTracker.Sdk.sln -c Release

The live smoke tests are opt-in and read-only apart from one instant check:

HT_BASE_URL=https://api2.host-tracker.com HT_TOKEN_FILE=/path/to/token \
  dotnet test -c Release --filter "Category=Live"

Licence

MIT - see LICENSE.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0 29 8/22/2026

See CHANGELOG.md.