Documentation

The site runs on one public API with eighteen instant check endpoints plus the public service-directory family. Each is documented below: what it does, its limits, and a request/response example. No account required; instant results are not stored. The management APIs at the end of this page (uptime monitoring, status pages, short links) are the stateful exception: they persist under their own management keys.

Status codes

  • 301 — Permanent redirect

    Permanent redirect. Search engines transfer ranking signals to the destination.

  • 302 — Temporary redirect

    Temporary redirect. The original URL stays canonical.

  • 307 — Temporary redirect

    Temporary redirect that preserves the request method and body.

  • 308 — Permanent redirect

    Permanent redirect that preserves the request method and body.

  • 200 — Success

    Final destination reached. The chain ends here.

  • 404 — Client error

    The destination does not exist — a broken redirect target.

  • 500 — Server error

    The destination resolved but the server failed to respond correctly.

Classified results, not errors

On the four instant endpoints, blocked targets, unresolvable domains and invalid URLs are not errors: every well-formed request returns HTTP 200 with a status that classifies the outcome. Only malformed requests, oversized bodies and rate limiting produce error responses.

Shared limits

Protocols
http, https
Method
GET
Rate limits
30 requests/minute and 500/day per IP — shared by all endpoints
Request body
up to 10 KiB, strict JSON

Service directory & community reports (Phase 14)

A curated, public directory of popular online services with anonymous community reports. Reads are public and cached; the report POST is the stateful exception — it persists and carries rate limiting, deduplication and moderation. Community signal and the automated check are always separate indicators; nothing here claims a service is confirmed down.

GET /api/v1/catalog/services?sort=reports_24h&page=1&page_size=20
→ 200
{ "items": [ { "slug": "chatgpt", "name": "ChatGPT",
               "category": { "slug": "ai-assistants", "name": "AI Assistants" },
               "community": { "signal": "reports_increasing", "reports_15m": 3,
                              "reports_1h": 11, "reports_24h": 42,
                              "last_report_at": "2026-10-20T09:58:12Z" },
               "automated_check": { "state": "operational",
                                    "checked_at": "2026-10-20T09:55:00Z" } } ],
  "page": 1, "page_size": 20, "total": 1 }

POST /api/v1/catalog/services/chatgpt/reports
{ "problem_type": "site_unreachable", "comment": "Login loop on mobile" }
→ 201
{ "service_slug": "chatgpt", "problem_type": "site_unreachable",
  "comment": "Login loop on mobile", "created_at": "2026-10-20T10:01:33Z" }

Reads are public and cached (ETag + max-age=30); unknown/draft/hidden
slugs all answer 404 (no existence oracle); merged slugs 301 to the
target. Reports are anonymous: no account, no network fetch on
submission, rate limited per IP, comments plain text (≤500 chars).
Community signal and the automated check are always separate blocks —
no value on this API means "confirmed down".
Pagination
page ≥1, page_size default 20 max 100; deterministic order (slug tie-break)
Report limits
dedup 1 per service per 10 min per IP, then 10/hour and 30/day per IP; 429 + Retry-After; fail-closed
Moderation & privacy
auto-publish or hold-all policy; kill switches; only a daily-rotating pseudonymous hash is stored — never raw IP or user-agent

Redirect Checker — POST /api/v1/check

Follows the full redirect chain and stores the result.

POST /api/v1/check
Content-Type: application/json

{ "url": "https://bit.ly/example" }

{
  "id": "9pr2kxq4",
  "status": "completed",
  "input_url": "https://bit.ly/example",
  "final_url": "https://example.com/product",
  "final_domain": "example.com",
  "final_status": 200,
  "redirect_count": 3,
  "duration_ms": 482,
  "redirects": [
    { "sequence": 0, "requested_url": "https://bit.ly/example",
      "status_code": 301, "next_url": "https://tracker.example/click",
      "duration_ms": 83 },
    { "sequence": 3, "requested_url": "https://example.com/product",
      "status_code": 200, "duration_ms": 184 }
  ],
  "errors": null
}

GET /api/v1/check/{id} — fetch a stored result by its public ID.
Maximum redirects
20 hops
Request timeout
15 seconds

GET /api/v1/check/{id} — fetch a stored result by its public ID.

Uptime Checker — POST /api/v1/uptime-check

Checks whether a site answers. The outcome is classified: UP for a healthy response, HTTP_ERROR when the site answers with 4xx/5xx, plus TIMEOUT, DNS_ERROR, TLS_ERROR, CONNECTION_ERROR and BLOCKED_TARGET.

POST /api/v1/uptime-check
Content-Type: application/json

{ "url": "https://example.com" }

{
  "url": "https://example.com",
  "status": "UP",
  "reachable": true,
  "httpStatus": 200,
  "httpStatusText": "OK",
  "responseTimeMs": 184,
  "finalUrl": "https://example.com/",
  "redirectCount": 1,
  "tls": { "enabled": true, "valid": true }
}
Maximum redirects
10 hops
Request timeout
10 seconds

HTTP Status Checker — POST /api/v1/http-status-check

Returns the final status code of a URL, the final response headers, a compact redirect-chain summary and the TLS state.

POST /api/v1/http-status-check
Content-Type: application/json

{ "url": "https://example.com" }

{
  "url": "https://example.com",
  "status": "UP",
  "reachable": true,
  "httpStatus": 200,
  "httpStatusText": "OK",
  "finalUrl": "https://example.com/",
  "redirectCount": 1,
  "headers": {
    "Content-Type": ["text/html; charset=utf-8"],
    "Server": ["nginx"]
  },
  "headers_analysis": {
    "security_missing": ["Strict-Transport-Security", "Content-Security-Policy"],
    "caching": { "Cache-Control": "max-age=604800" }
  },
  "tls": { "enabled": true, "valid": true }
}
Maximum redirects
10 hops
Request timeout
10 seconds

Response Time Checker — POST /api/v1/response-time-check

Measures 1–5 samples and classifies a speed verdict from the median time to first byte. Returns per-sample durations, min/median/max/mean, and the final-hop timing breakdown.

POST /api/v1/response-time-check
Content-Type: application/json

{ "url": "https://example.com", "samples": 3 }

{
  "url": "https://example.com",
  "samples": 3,
  "status": "UP",
  "verdict": "FAST",
  "completed": 3,
  "timesMs": [184, 201, 176],
  "stats": { "minMs": 176, "medianMs": 184, "maxMs": 201, "meanMs": 187 },
  "breakdown": { "dnsMs": 12, "connectMs": 23, "tlsMs": 45, "ttfbMs": 184, "totalMs": 190 },
  "httpStatus": 200,
  "httpStatusText": "OK",
  "finalUrl": "https://example.com/",
  "redirectCount": 1,
  "tls": { "enabled": true, "valid": true }
}
Samples
1–5 (default 3)
Per-sample timeout
10 seconds
Run deadline
30 seconds

SSL Checker — POST /api/v1/ssl-check

Inspects the TLS certificate a host presents: validity, issuer, SANs, negotiated protocol and cipher, and the chain. A TLS-only probe — the page is never fetched; a broken certificate is reported with a concrete reason.

POST /api/v1/ssl-check
Content-Type: application/json

{ "url": "https://example.com" }

{
  "url": "https://example.com",
  "host": "example.com",
  "status": "UP",
  "reachable": true,
  "handshakeMs": 45,
  "tls": {
    "version": "TLS 1.3",
    "cipher": "TLS_AES_256_GCM_SHA384",
    "verified": true,
    "status": "valid",
    "daysRemaining": 89,
    "notBefore": "2026-08-01T00:00:00Z",
    "notAfter": "2026-10-30T00:00:00Z",
    "subject": "CN=example.com",
    "issuer": "CN=R3,O=Let's Encrypt,C=US",
    "sans": ["example.com", "www.example.com"],
    "chain": ["CN=example.com", "CN=R3,O=Let's Encrypt,C=US"]
  }
}
Port
from the URL (default 443)
Probe timeout
10 seconds

DNS Checker — POST /api/v1/dns-check

Looks up A, AAAA, CNAME, MX, TXT and NS records for a host. Pure lookup-and-report: no connections are made, and every resolved address is labeled public or non-public.

POST /api/v1/dns-check
Content-Type: application/json

{ "url": "example.com" }

{
  "url": "example.com",
  "host": "example.com",
  "status": "UP",
  "durationMs": 38,
  "records": {
    "a": ["93.184.216.34"],
    "aaaa": ["2606:2800:220:1:248:1893:25c8:1946"],
    "cname": [],
    "mx": [{ "host": "mail.example.com", "pref": 10 }],
    "txt": ["v=spf1 include:_spf.example.com ~all"],
    "ns": ["ns1.example.com", "ns2.example.com"]
  },
  "addressLabels": [
    { "address": "93.184.216.34", "classification": "public", "reason": "public" }
  ]
}
Record families
A, AAAA, CNAME, MX, TXT, NS
Lookup timeout
10 seconds overall, 3 seconds per family

Domain Expiry Checker — POST /api/v1/domain-expiry-check

Answers when a domain expires via the fixed rdap.org RDAP flow: verdict, days remaining, registration/expiration dates and registry statuses. The repo's first fixed-third-party outbound tool: HTTPS-only hops, no proxy, bounded redirects and size; the queried domain is never contacted. 404 maps to a not_found verdict (unregistered / registry without RDAP / nothing published), 429 and 5xx to honest UNKNOWN_ERROR.

POST /api/v1/domain-expiry-check
Content-Type: application/json

{ "url": "example.com" }

{
  "url": "example.com",
  "host": "example.com",
  "status": "UP",
  "verdict": "ok",
  "daysRemaining": 214,
  "registration": "2019-01-15T05:00:00Z",
  "expiration": "2027-04-15T04:00:00Z",
  "lastChanged": "2026-08-01T11:20:00Z",
  "registryStatuses": ["ok"],
  "source": "rdap",
  "durationMs": 420
}
Verdicts
ok, expiring (≤30 days), expired, not_found, unknown
Lookup timeout
10 seconds overall (DOMAIN_EXPIRY_TIMEOUT)
Outbound posture
rdap.org only; HTTPS-only every hop; proxy disabled; redirect cap 10; 1 MiB size cap; the queried domain is never contacted

WHOIS Lookup — POST /api/v1/whois-check

Answers what the registry knows about a domain: registrar, nameservers, dates and status flags. RDAP-first via rdap.org, falling back to classic WHOIS (raw TCP 43) for zones without RDAP — the repo's first raw-TCP outbound tool: every referral must be a host[:43] target, is resolved through the shared SSRF pipeline and dialed at the pinned public address; referral cap 2, 64 KiB response cap; the queried domain is never contacted. 404 from RDAP triggers the fallback; a definitive no-such-domain answer surfaces as notFound.

POST /api/v1/whois-check
Content-Type: application/json

{ "url": "mail.ru" }

{
  "url": "mail.ru",
  "host": "mail.ru",
  "status": "UP",
  "source": "whois",
  "registrar": "VK-TECH",
  "nameservers": ["ns1.mail.ru", "ns2.mail.ru"],
  "registration": "1997-10-01T00:00:00Z",
  "expiration": "2026-10-01T00:00:00Z",
  "statuses": ["REGISTERED, DELEGATED, VERIFIED"],
  "durationMs": 640
}
Sources
RDAP via rdap.org; classic WHOIS (TCP 43) via the whois.iana.org referral chain
Lookup timeout
15 seconds overall (WHOIS_TIMEOUT)
Outbound posture
referrals host[:43] only; SSRF-validated and pinned before dialing; cap 2 referrals; 64 KiB cap; the queried domain is never contacted

Domain Availability — POST /api/v1/domain-availability

Answers is this name registrable, per TLD, with an honest state machine: AVAILABLE only when the RDAP lookup answered 404 and the embedded TLD metadata confirms the registry operates RDAP; a WHOIS-only not-found stays UNKNOWN (likely available); RESTRICTED is metadata-driven. Full IDN support (Unicode in, punycode canonical, displayDomain out) and optional multi-TLD fan-out (1–10 per request). Premium is always unknown in MVP — pricing is a separate future subsystem.

POST /api/v1/domain-availability
Content-Type: application/json

{ "domain": "пример.рф", "tlds": ["com", "io"] }

{
  "url": "пример.рф",
  "displayDomain": "пример.рф",
  "host": "xn--e1afmkfd.xn--p1ai",
  "status": "UP",
  "results": [
    {
      "tld": "com",
      "availability": "AVAILABLE",
      "reason": "rdap_not_found",
      "confidence": "high",
      "source": "rdap",
      "premium": "unknown",
      "checkedAt": "2026-09-03T12:00:02Z"
    },
    {
      "tld": "io",
      "availability": "UNKNOWN",
      "reason": "tld_rdap_unknown",
      "confidence": "low",
      "source": "tld-metadata",
      "premium": "unknown",
      "checkedAt": "2026-09-03T12:00:02Z"
    }
  ],
  "tldMetaVersion": "2026-09-02",
  "durationMs": 1240
}
TLDs per request
1–10 unique (Unicode or punycode); at most 4 lookups in flight under one 15-second budget (DOMAIN_AVAILABILITY_TIMEOUT)
Verdicts
AVAILABLE | REGISTERED | RESTRICTED | INVALID | UNKNOWN | ERROR — UNKNOWN is never laundered into AVAILABLE; PREMIUM is unreachable in MVP
Caching
di:avail 60s, di:reg 6h, fail-open; negative results are never cached

Domain Age — POST /api/v1/domain-age

Reports how old a domain is: the exact creation date from the registry and a deterministic years/months/days breakdown. Thin projection over the WHOIS data — one provider fetch serves availability, age and registrar within the cache window.

POST /api/v1/domain-age
Content-Type: application/json

{ "domain": "example.com" }

{
  "url": "example.com",
  "host": "example.com",
  "status": "UP",
  "age": {
    "known": true,
    "createdAt": "2019-01-15T05:00:00Z",
    "ageInDays": 2809,
    "years": 7,
    "months": 7,
    "days": 20
  },
  "source": "rdap",
  "durationMs": 390
}
Age math
UTC calendar dates only; full years/months via a clamped-month anchor; total days floored
Unknown age
age.known=false + bounded reason (no_data, inconsistent_data) — never a guess
Caching
di:reg 6h shared with availability/registrar (fail-open)

Domain Registrar Checker — POST /api/v1/domain-registrar

Reports which registrar a domain is registered with, normalized against a curated alias table (GoDaddy, Namecheap, Cloudflare and more) — unknown names are shown exactly as published. Registry organization and RDAP/WHOIS provenance included; shares the registration cache with age/availability.

POST /api/v1/domain-registrar
Content-Type: application/json

{ "domain": "example.com" }

{
  "url": "example.com",
  "host": "example.com",
  "status": "UP",
  "registrar": "GoDaddy.com, LLC",
  "registrarCanonical": "GoDaddy",
  "registrarIanaId": 625,
  "registry": "Verisign, Inc.",
  "source": "rdap",
  "checkedAt": "2026-09-04T12:00:00Z",
  "durationMs": 410
}
Registrar
display name + canonical alias badge when matched; IANA ID shows “—” (registry data does not carry it yet)
Not published
privacy-proxied or redacted registrations answer UP with reason no_data — never an empty shell

Domain Generator — POST /api/v1/domain-generate

Generates candidate domain names from 1–3 keywords: direct forms, prefixes, suffixes, compounds and short forms — deduplicated, ordered and capped. Pure and instant: no registry calls during generation; per-candidate availability checks are separate explicit actions.

POST /api/v1/domain-generate
Content-Type: application/json

{ "keywords": ["coffee", "map"], "tlds": ["com", "io"], "limit": 24 }

{
  "status": "UP",
  "candidates": [
    { "domain": "coffee.com", "strategy": "direct" },
    { "domain": "coffee.io", "strategy": "direct" },
    { "domain": "getcoffee.com", "strategy": "prefix" },
    { "domain": "coffeemap.com", "strategy": "compound" }
  ],
  "total": 24
}
Candidates
hard cap 50, 24 shown by default; ≤10 TLDs filtered through the TLD metadata snapshot
No provider traffic
generation makes zero registry calls — per-candidate checks hit the availability endpoint with its own caps

Domain Overview — POST /api/v1/domain-overview

One composed profile: availability on the domain’s own TLD, creation date with the deterministic age breakdown, registrar (normalized, with IANA number), expiry projection and the registry’s configuration findings. Sections are independent — one failing lookup never blanks the others.

POST /api/v1/domain-overview
Content-Type: application/json

{ "domain": "example.com" }

{
  "url": "example.com",
  "host": "example.com",
  "status": "UP",
  "availability": {
    "tld": "com", "availability": "REGISTERED", "reason": "registered",
    "confidence": "high", "source": "rdap", "premium": "unknown",
    "checkedAt": "2026-09-05T12:00:00Z"
  },
  "registration": {
    "known": true, "createdAt": "2019-01-15T05:00:00Z",
    "ageYears": 7, "ageMonths": 7, "ageDays": 21,
    "registrar": "GoDaddy.com, LLC", "registrarCanonical": "GoDaddy",
    "registrarIanaId": 625, "registry": "Verisign, Inc.", "source": "rdap",
    "checkedAt": "2026-09-05T12:00:01Z"
  },
  "expiry": {
    "host": "example.com", "source": "rdap", "verdict": "ok",
    "daysRemaining": 222, "expiration": "2027-04-15T04:00:00Z",
    "checkedAt": "2026-09-05T12:00:02Z"
  },
  "holds": [],
  "dnssec": [{ "id": "dnssec-absent", "state": "info", "severity": "info", "detail": "unsigned" }],
  "tldMetaVersion": "2026-09-02",
  "durationMs": 1620
}
Sections
availability (metadata-gated) + registration (di:reg, 6h shared) + expiry (rdap.org) + holds/dnssec findings
Findings are facts
holds/DNSSEC are registry configuration signals — never safety verdicts; DNS/SSL stay with their standalone tools

Security Checker — POST /api/v1/security-check

A passive, advisory configuration review of one fetched response: security headers (HSTS depth, CSP highlights, COOP/COEP/CORP, X-XSS-Protection), cookie flags, TLS facts, chain downgrades and disclosure. No probing, no vulnerability assertions, no grades; cookie values never leave the backend. HTTP application status does not gate the review (a reachable 404/500 is reviewed).

POST /api/v1/security-check
Content-Type: application/json

{ "url": "https://example.com" }

{
  "url": "https://example.com",
  "finalUrl": "https://example.com/",
  "status": "UP",
  "durationMs": 610,
  "summary": { "total": 14, "passed": 9, "warn": 3, "failed": 2 },
  "findings": [
    { "id": "hsts-present", "state": "fail", "severity": "high" },
    { "id": "tls-cert", "state": "pass", "severity": "info", "detail": "valid" }
  ],
  "cookies": [
    { "name": "session", "secure": false, "httpOnly": true, "sameSite": "Lax" }
  ],
  "tls": { "enabled": true, "version": "TLS 1.3", "certStatus": "valid", "daysRemaining": 89 }
}
Scope
~23 check IDs across headers/cookies/TLS/chain/disclosure; passive fetch only
Cookies
Names and flags only (Secure/HttpOnly/SameSite) — values never leave the backend
Review timeout
10 seconds overall (SECURITY_CHECK_TIMEOUT)

URL Analyzer — POST /api/v1/url-analyze

A pure syntactic breakdown of one URL: redacted canonical identity (scheme, host, IDN forms, naive registrable domain, port, path, query, fragment), ordered duplicate-preserving parameters with versioned tracking classification, tracking counts, informational security signals, and normalization notes. No request, no DNS, no persistence — the analysis is local and sub-millisecond.

POST /api/v1/url-analyze
Content-Type: application/json

{ "url": "https://xn--e1afmkfd.xn--p1ai/search?q=thing&utm_source=news&id=7&id=8" }

{
  "status": "UP",
  "identity": {
    "hasCredentials": false,
    "scheme": "https",
    "host": "xn--e1afmkfd.xn--p1ai",
    "displayHost": "пример.рф",
    "registrableDomain": "пример.рф",
    "isIdn": true,
    "path": "/search",
    "query": "q=thing&utm_source=news&id=7&id=8",
    "normalized": "https://xn--e1afmkfd.xn--p1ai/search?q=thing&utm_source=news&id=7&id=8"
  },
  "parameters": [
    { "name": "q", "value": "thing", "classification": { "category": "unknown", "confidence": "none" } },
    { "name": "utm_source", "value": "news", "classification": { "category": "utm", "label": "utm_source", "confidence": "exact" } },
    { "name": "id", "value": "7", "occurrence": 1 },
    { "name": "id", "value": "8", "occurrence": 2 }
  ],
  "tracking": { "utm": 1, "analytics": 0, "adsClickId": 0, "sessionSensitive": 0, "unknown": 3 },
  "signals": ["idn_host"],
  "normalization": { "schemeAssumed": false, "punycodeApplied": true, "defaultPortOmitted": false, "hostLowercased": false },
  "registryVersion": "2026-09-1"
}
Scope
Components + ordered parameters + tracking registry classification + informational signals; zero network
Redaction
Passwords never echoed; session-sensitive parameter values masked (***) everywhere, names visible

URL Expander — POST /api/v1/url-expand

Instant, persistence-free expansion of a redirect chain over the shared engine: every hop passes the same central SSRF validation, loops and the 10-redirect hop cap are detected with distinct reasons, and the final URL gets a full tracking analysis. Echoed URLs are redacted — passwords never leave the backend. Not cached: chains are time-sensitive, freshness is the correct answer.

POST /api/v1/url-expand
Content-Type: application/json

{ "url": "https://bit.ly/3xyz" }

{
  "url": "https://bit.ly/3xyz",
  "status": "UP",
  "finalUrl": "https://example.com/landing?utm_source=n",
  "hopCount": 2,
  "chain": [
    { "url": "https://bit.ly/3xyz", "status": 301, "location": "https://bit.ly/abc" },
    { "url": "https://bit.ly/abc", "status": 302, "location": "https://example.com/landing?utm_source=n" }
  ],
  "analysis": { "status": "UP", "tracking": { "utm": 1, "analytics": 0, "adsClickId": 0, "sessionSensitive": 0, "unknown": 0 } },
  "source": "engine",
  "durationMs": 820
}
Scope
Redacted hop chain + classified status (loop vs cap distinguished) + final-URL analysis; no per-hop DNS/TLS/timing (Redirect Checker's domain)
Expansion budget
10 seconds overall (URL_EXPAND_TIMEOUT); hop cap 10

UTM Builder — POST /api/v1/utm-build

Deterministic, stateless campaign-URL construction: unrelated existing parameters are preserved in order, canonical UTMs are overwritten or kept by field, duplicates collapse to the last occurrence, the fragment is preserved, and credential-bearing destinations are refused in-band. Same input always produces byte-identical output.

POST /api/v1/utm-build
Content-Type: application/json

{ "url": "https://example.com/landing?foo=1&utm_source=old#pricing", "utmSource": "newsletter", "utmMedium": "email", "utmCampaign": "spring_sale", "utmContent": "header_cta" }

{
  "status": "UP",
  "generatedUrl": "https://example.com/landing?foo=1&utm_source=newsletter&utm_medium=email&utm_campaign=spring_sale&utm_content=header_cta#pricing",
  "warnings": ["existing_utm_overwritten:utm_source", "empty_field_skipped:utm_term"],
  "parameters": { "utm_source": "newsletter", "utm_medium": "email", "utm_campaign": "spring_sale", "utm_content": "header_cta" }
}
Scope
Fixed-order UTM appending + preserve/overwrite/collapse policy + warnings; pure, no network
Determinism
Byte-identical output for identical input; fields ≤256 chars; unknown request fields rejected

Website Health — POST /api/v1/website-health

The composite diagnostic: one request normalizes the target and runs bounded concurrent lanes above the existing engines — one HTTP check feeds availability, status, response time, headers and security configuration; DNS, the TLS certificate probe and the domain profile run as their own lanes. The verdict is an aggregate overall state with per-check details and deep links to the standalone tools. Findings describe the performed checks only — never a security guarantee.

POST /api/v1/website-health
Content-Type: application/json

{ "url": "https://example.com" }

{
  "target": {
    "canonical": "https://example.com/",
    "displayHost": "example.com",
    "scheme": "https",
    "hasCredentials": false,
    "isDomain": true,
    "isHttp": true,
    "valid": true
  },
  "overall": "WARNING",
  "summaryKey": "health.summary.warning",
  "checks": [
    { "type": "http_availability", "state": "pass",
      "summaryKey": "health.check.httpAvailability.pass",
      "value": "200", "durationMs": 612, "applicable": true,
      "detailUrl": "/uptime-checker" },
    { "type": "http_status", "state": "pass",
      "summaryKey": "health.check.httpStatus.pass",
      "value": "200 OK", "applicable": true,
      "detailUrl": "/http-status-checker" },
    { "type": "response_time", "state": "pass",
      "summaryKey": "health.check.responseTime.pass",
      "value": "612 ms", "applicable": true,
      "detailUrl": "/response-time-checker" },
    { "type": "headers", "state": "warn",
      "summaryKey": "health.check.headers.warn",
      "value": "2 missing", "reason": "findings", "applicable": true,
      "detailUrl": "/security-checker" },
    { "type": "security", "state": "warn",
      "summaryKey": "health.check.security.warn",
      "value": "14 of 21 passed", "reason": "findings", "applicable": true,
      "detailUrl": "/security-checker" },
    { "type": "tls", "state": "pass",
      "summaryKey": "health.check.tls.pass",
      "value": "TLS 1.3 · valid, 89 days", "durationMs": 140,
      "applicable": true, "detailUrl": "/ssl-checker" },
    { "type": "dns", "state": "pass",
      "summaryKey": "health.check.dns.pass",
      "value": "A, AAAA, MX, NS", "durationMs": 95,
      "applicable": true, "detailUrl": "/dns-checker" },
    { "type": "domain", "state": "pass",
      "summaryKey": "health.check.domain.pass",
      "value": "expires in 214 days", "durationMs": 730,
      "applicable": true, "detailUrl": "/domain-overview" }
  ],
  "meta": { "checksTotal": 8, "checksPassed": 6, "checksWarn": 2,
            "checksFailed": 0, "checksUnknown": 0,
            "tldMetaVersion": "2026-09-03", "durationMs": 1480 }
}
Scope
One HTTP execution feeds five sections; DNS/TLS/domain lanes; aggregate HEALTHY/WARNING/ERROR/PARTIAL/UNKNOWN
Bounds
25s parent budget + per-lane budgets, ≤3 lanes in flight; invalid targets answered in-band; no cache — freshness first

Uptime Monitoring — the /api/v1/monitors management API

The stateful exception to the instant tools: create a monitor for one URL and the worker checks it on a fixed schedule — with execution history, uptime statistics, incident episodes and webhook notifications. Ownership is a management key shown exactly once at creation; every read and write authenticates with it, and a wrong key is indistinguishable from a missing monitor (uniform 404).

POST /api/v1/monitors
Content-Type: application/json

{ "url": "https://example.com", "name": "Prod site", "interval": 300 }

201 Created
{
  "monitor": { "id": "b1…", "name": "Prod site",
               "normalizedTarget": "https://example.com/",
               "state": "unknown", "interval": 300 },
  "manageUrl": "/monitors/b1…?key=…",
  "key": "…shown exactly once…"
}

GET /api/v1/monitors/{id}?key=…       detail + stats window + incidents
GET /api/v1/monitors/{id}/history     cursor-paginated executions
GET /api/v1/monitors/{id}/history/daily  per-day buckets (30-day cap)
GET /api/v1/monitors/{id}/incidents   incident episodes
POST /api/v1/monitors/{id}/check      enqueue a priority manual check
PATCH /api/v1/monitors/{id}           name / interval / webhook / pause
DELETE /api/v1/monitors/{id}          cascade delete

Credentials in the target are rejected (credentials_not_allowed); the
target is SSRF-pre-validated at creation and re-validated on every run.
Scope
Create / list / detail / pause / delete / manual check / history / incidents; fixed intervals 60–3600s; in-band INVALID_URL and BLOCKED_TARGET at creation
Key management
256-bit key shown once (sha256-stored); credentials in targets rejected; SSRF pre-validated at creation and re-validated on every run

Public Status Pages — the /api/v1/status-pages management API

Compose your monitors into a shareable status page: pick components, order them, override display names and publish under a stable slug. Pages live under the same management key as the monitors; drafts are private, publishing is idempotent and reversible, and every mutation deletes the public cache entry so access changes apply within the request.

POST /api/v1/status-pages
Content-Type: application/json
X-Monitor-Key: <your key>

{ "name": "Prod API" }

201 Created
{
  "page": { "publicId": "p1…", "name": "Prod API",
            "slug": "prod-api", "published": false },
  "manageUrl": "/status-pages/p1…?key=…"
}

GET  /api/v1/status-pages/{id}?key=…   editor payload (members + monitors)
PATCH /api/v1/status-pages/{id}        name/description/slug + membership
POST /api/v1/status-pages/{id}/publish      → live at /status/{slug}
POST /api/v1/status-pages/{id}/unpublish    → removed within the request
DELETE /api/v1/status-pages/{id}       cascade delete

GET /api/v1/public/status-pages/{slug}   the public DTO (no key; cached 60s)

Membership is same-key only (≤25 monitors, replace-list semantics);
publish requires ≥1 visible member; the public DTO is an explicit
projection — target URLs, keys and internal ids never cross.
Scope
Create / list / edit / membership replace-list (≤25, same key only) / publish / unpublish / delete; slugs 3-63 chars with a reserved-word list; max 5 pages per key
Public surface
GET /api/v1/public/status-pages/{slug} + /status/{slug}; explicit public DTO only — target URLs, keys, notification hosts and internal ids never cross; Redis-cached 60s, fail-open

Turn a long URL into a short /go/{code} link you can manage: validated destination, crypto-random code, click statistics with today/7-day/30-day windows. Links live under the same management key as your monitors and status pages — creation attaches to an existing key and never mints a new one. Destinations are validated (syntax, credentials rejected, SSRF classification) when you create or edit; the public redirect itself never fetches anything and answers 302 with no caching.

POST /api/v1/short-links
Content-Type: application/json
X-Monitor-Key: <your key>

{ "destinationUrl": "https://example.com/launch?utm_source=newsletter" }

201 Created
{
  "link": { "id": "sl1…", "code": "a3f9k2mz",
            "shortUrl": "/go/a3f9k2mz",
            "destination": "https://example.com/launch?utm_source=newsletter" }
}

GET    /api/v1/short-links?key=…              your links (newest first)
GET    /api/v1/short-links/{id}?key=…         full link + stats
PATCH  /api/v1/short-links/{id}?key=…         destination/title/expiry (re-validated)
POST   /api/v1/short-links/{id}/disable       public 404, stats kept (enable restores)
DELETE /api/v1/short-links/{id}?key=…         soft delete (idempotent)
GET    /api/v1/short-links/{id}/stats?key=…   total + today/7d/30d clicks

GET /go/a3f9k2mz   → 302 Found + Location (Cache-Control: no-store);
                     unknown/disabled/expired/deleted all answer 404.

Destinations are validated at create/update (http/https, credentials
rejected, SSRF classification); the redirect itself never fetches.
Scope
Create / list / edit / disable / enable / soft-delete / stats; ≤100 active links per key, 30 creations per key per hour; destination re-validated on every change
Public redirect
GET /go/{code} → 302 + no-store; unknown/disabled/expired/deleted answer the same neutral 404; the redirect is a pure lookup — it never accepts a destination parameter and never resolves or fetches the target; no IPs, no user-agents stored

If a /go/{code} link looks like phishing, malware, spam or a scam, report it — no account needed and you will not be contacted back. Reports land in the operator's review queue; only the operator can take a link down. The endpoint answers the same confirmation for every well-formed report, so a reporter learns nothing about whether a code exists.

POST /api/v1/short-link-reports
Content-Type: application/json

{ "code": "a3f9k2mz", "reason": "phishing",
  "detail": "Imitates a login page" }

202 Accepted
{ "status": "received" }

Reasons: phishing | malware | spam | scam | other (detail ≤500 chars,
optional plain text). The SAME 202 comes back for unknown, disabled
and deleted codes — no existence oracle. Limits per IP: one report
per code per 10 min, 10/hour, 30/day (429 + Retry-After; Redis
failure = 503, fail-closed). Reports go to the operator queue; only
the operator can take a link down (the owner key cannot override).
Limits & privacy
1 report per code per 10 min per IP, then 10/hour and 30/day (429 + Retry-After; fail-closed on Redis errors); reasons phishing | malware | spam | scam | other; no account, no email, no raw IP stored — a daily-rotating fingerprint only
Uniform response
every well-formed submission gets the same 202 received — for live, disabled, deleted and unknown codes alike (no existence oracle); malformed codes or unknown reasons answer 400 before any lookup

Accounts & API Keys — /api/v1/auth/register and /api/v1/api-keys

Browser accounts (email + password, open registration; the email is unverified in this version) with server-side sessions in a hardened cookie. API keys authenticate server-to-server calls over an explicit allowlist of the resource families. Login failures are generic — unknown email and wrong password are indistinguishable, including by timing.

POST /api/v1/auth/register
Content-Type: application/json

{ "email": "you@example.com", "password": "…", "displayName": "…" }

201 Created + Set-Cookie: wg_session (HttpOnly; Secure; SameSite=Lax)
{ "account": { "id": "acc…", "email": "you@example.com", … } }

POST /api/v1/auth/login     200 + Set-Cookie (401 generic on failure)
POST /api/v1/auth/logout    deletes the server-side session
GET  /api/v1/account        the session's own profile
PATCH /api/v1/account       update display name (displayName)
GET  /api/v1/account/summary  account-owned resource counts
POST /api/v1/account/claim  management key → re-own key-held resources

POST /api/v1/api-keys               (SESSION) create; secret shown ONCE
{ "name": "CI", "scopes": ["monitors:read"] }
GET  /api/v1/api-keys               (SESSION) list — no secrets
POST /api/v1/api-keys/{id}/revoke   (SESSION) immediate 401 on next use

Bearer access (Authorization: Bearer wg_live_…) is an EXPLICIT
allowlist only: account GET/PATCH, monitors CRUD + reads, status-pages
GET, short-links CRUD + stats; everything else refuses API keys.
Missing/unknown/revoked → generic 401; wrong scope → 403; per-key and
per-account rate limits → 429 + Retry-After.
Bearer allowlist
account GET/PATCH (account:read), monitors (read/write), status-pages GET (read), short-links (read/write); everything else refuses API keys. Unclaimed key-owned resources are invisible until attached via claim-by-key.
Secret handling
The full wg_live_ key is shown exactly once at creation; only a one-way hash is stored. Missing/malformed/unknown/revoked keys answer one generic 401; wrong scope answers 403; per-key and per-account rate limits answer 429 + Retry-After.

Account email identity (verification & recovery)

Phase J email foundation: confirm your address, recover a lost password and prove recent password knowledge for sensitive actions.

POST /api/v1/auth/verification/request     (SESSION) 202 {accepted:true}
POST /api/v1/auth/verification/confirm     { "token": "…" } → 200 {verified:true}
POST /api/v1/auth/password-reset/request   { "email": "you@example.com" } → 202 (always the same answer)
POST /api/v1/auth/password-reset/confirm   { "token": "…", "password": "…" } → 200 {reset:true}
POST /api/v1/auth/reauthenticate           (SESSION) { "password": "…" } → 200 {confirmed:true}

Reset revokes ALL sessions in the same transaction; API keys stay
active. Tokens ride mail links as URL fragments — never query strings —
and every mismatch (wrong account, replay, expiry, unknown) answers the
same generic 404.
Tokens
one-time, 24h (verify) / 30m (reset), fragment-only, generic 404 on any mismatch
Reset
revokes all sessions atomically; API keys stay valid
Re-auth proof
5 minutes, server-side per session
Workspaces
GET /api/v1/workspaces — personal + org memberships, no-store, session-only
Organizations
create/list/read/rename/delete + ownership transfer; verified sessions, optimistic versions, reauth-gated destructive actions
Invitations
email invites with 7-day single-use links, four fixed roles, seat-reserving caps and required rate windows
Organization resources
shared monitors/pages/links under /organizations/{orgId}; session RBAC, versioned mutations, legacy keys excluded in SQL
Resource transfer
move personal monitors/pages/links into an organization atomically (closure ≤100, version-checked, idempotent)
Organization keys
org-scoped service keys (wg_live_…), shown once, scoped, survive creator departure, revocation immediate
Organization audit
append-only history of committed org changes (365 days), Owner/Admin readable, least-disclosure actors