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
URL Shortener — the /api/v1/short-links management API
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
Report a short link — the anonymous abuse-report API
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