Lifub API Reference

Version 1.0.3OpenAPI 3.1.0Base URL https://api.lifub.comOpenAPI JSON

Every endpoint below is generated from the same document, so this page and the JSON cannot disagree.

echo

Request echo and fraud detection endpoints

GET /echo.json

Echo request details with fraud detection
Returns detailed information about the incoming request including headers, IP address, fingerprint, session hash, and fraud detection signals. Useful for debugging, bot detection, and request inspection. Pass `?status=NNN` to override the default 200 response code (any valid HTTP status 100–999; invalid/absent values fall back to 200).
Responses
StatusContentDescription
200application/json EchoEcho response with request details
500—Internal server error
Example request
curl -sS 'https://api.lifub.com/echo.json'

entropy

Random number generation

GET /entropy.json

Generate random entropy values
Returns a collection of random values including UUID, integers, floats, and a Gaussian-distributed number. Uses a fast (non-cryptographic) RNG.
Responses
StatusContentDescription
200application/json EntropyResponseEntropy values generated successfully
500—Internal server error
Example request
curl -sS 'https://api.lifub.com/entropy.json'

encoding

Text encoding utilities (MD5, SHA1, SHA256, Base64)

GET /encoding.json

Encode and hash text values
Computes various encodings (Base64, URL) and hashes (MD5, SHA1, SHA256) for the given input value. Also provides octal, decimal, and hex byte representations.
Parameters
NameInTypeRequiredDescription
valuequerystringnoRaw input value to encode/decode.
charsetquerystringnoCharset name, e.g. `UTF-8`, `ISO-8859-1`, etc. Defaults to `UTF-8` (matching the Kotlin version's `Charsets.UTF_8.name()`).
Responses
StatusContentDescription
200application/json EncodingResponseEncoding results
500—Internal server error
Example request
curl -sS 'https://api.lifub.com/encoding.json'

whois

IP geolocation and WHOIS lookup

GET /whois.json

IP geolocation and ASN lookup
Returns GeoIP + ASN data for the caller's IP: country, city, ASN and ISP. Data source: DB-IP Lite (CC BY 4.0). Free-tier responses include the extended fields (`code` postal, `timeZone`, `countryState`, `cityId`, `countryStateId`) in the response shape with sentinel placeholders — contact info@lifub.com to switch a key onto the Pro/Enterprise MaxMind backend that returns real values. This is IP intelligence, NOT RDAP / domain-registration WHOIS. Auth: anonymous callers are rate-limited per IP; authenticated callers pass a key via the `X-API-Key` header, which also governs the monthly quota for that key. The optional `clientId` query parameter is a client-supplied request ID for log correlation; it is NOT used for authentication or billing.

Auth Optional X-API-Key: <API key> - Optional API key. Anonymous callers are rate-limited per IP; a key raises the limit and meters usage.

Parameters
NameInTypeRequiredDescription
queryIPquerystringnoOverride IP address for lookup
clientIdquerystringnoOptional client-supplied request ID for log correlation. NOT auth.
Responses
StatusContentDescription
200application/json WhoisGeoIP + ASN data for the caller or the overridden IP
429—Rate limit or monthly quota exceeded
500—Internal server error
Example request
curl -sS 'https://api.lifub.com/whois.json'

inspect

Request inspection utilities

GET /inspect.json

Request inspection combined with IP geolocation and ASN
Returns the incoming request (headers, request-body echo, a per-request fingerprint, and ASN-bumped fraud / bot-likelihood signals) combined with GeoIP + ASN data for the caller. Data source: DB-IP Lite (CC BY 4.0); extended geolocation fields (postal, time zone, subdivision) return sentinel placeholders unless the key is on the Pro/Enterprise MaxMind backend. This is IP intelligence, NOT RDAP / domain-registration WHOIS. Auth: anonymous callers are rate-limited per IP; authenticated callers pass a key via the `X-API-Key` header, which also governs the monthly quota for that key. Any tier (Free / Pro / Enterprise) may call this endpoint — plan gating is on quota and extended fields, not on endpoint access. Supports the same optional `queryIP` override and `clientId` log-correlation parameters as `/whois.json`. The debug-only `response-header` parameter echoes caller-specified headers back in the response, restricted to a small allowlist of safe cache / content-negotiation headers.

Auth Optional X-API-Key: <API key> - Optional API key. Anonymous callers are rate-limited per IP; a key raises the limit and meters usage.

Parameters
NameInTypeRequiredDescription
queryIPquerystringnoOverride IP address for geolocation lookup
clientIdquerystringnoOptional client-supplied request ID for log correlation. NOT auth.
response-headerquerystringnoDebug: echo `Name: Value` back as a response header. Restricted to a safe allowlist.
Responses
StatusContentDescription
200application/json InspectionInspection data combining request echo and IP geolocation
429—Rate limit or monthly quota exceeded
500—Internal server error
Example request
curl -sS 'https://api.lifub.com/inspect.json'

curl

HTTP fetch proxy

GET /curl.json

HTTP fetch proxycostly
Fetches the specified URL and returns the response status code and body. Targets must be public http/https endpoints; loopback, RFC 1918, link-local, and other non-routable addresses are refused. Response body is capped at 5 MiB.
Parameters
NameInTypeRequiredDescription
urlquerystring | nullnoURL to fetch
Responses
StatusContentDescription
200application/json CurlResponseSuccessfully fetched URL
400—URL missing, malformed, or pointing at a disallowed target
404—Failed to fetch URL (network error, etc.)
413—Upstream body exceeded the 5 MiB cap
502—DNS resolution for the target failed
Example request
curl -sS 'https://api.lifub.com/curl.json'

audit

Bounded HTTP/security and technical SEO site audits

GET /audit.json

Audit one page's HTTP/security health and technical SEO readinesscostly
Fetches a public page and a bounded set of same-origin well-known files, plus /llms.txt for the unweighted AI readiness checks; /llms.txt is reported separately under supplementalProbes and never enters probes, coverage, or any score. The page request is repeated once with compression enabled for the advisory performance section, which also reports a single lab timing sample, the negotiated HTTP version, and source-HTML speed signals; it is not a Core Web Vitals measurement and never changes any score, JUnit case, or snapshot diff. When the deployment configures a Chrome UX Report API key, performance.fieldData carries Google's public, aggregated real-user Core Web Vitals at the 75th percentile for the page URL, or for its origin when Google has no record for the URL alone; that lookup sends Google only the URL without its query string or fragment, and if needed its origin, and never fails the audit. Without a key, fieldData.state is notConfigured. The legacy HTTP/security health score and the separately versioned source-HTML technical SEO score have independent opt-in gates; neither is a ranking score. Anonymous callers receive 10 completed reports, then a full one-hour cooldown starts when the tenth report completes. An operator-issued X-Site-Audit-Voucher bearer UUID adds 100 completed reports and bypasses proof-of-work, but not scanner safety limits.

Auth Optional X-Site-Audit-Voucher: <audit voucher> - Operator-issued Site Audit voucher UUID. Optional on GET /audit.json and POST /audit/snapshots, where it adds paid reports and skips the proof of work. Required on GET /audit/quota, which reports the voucher's remaining paid reports. or X-Site-Audit-Trial: <audit trial> - Operator-issued, host-scoped and expiring Site Audit trial. Required on GET /audit/trial and for TLS-only watch creation; optional on GET /audit.json and POST /audit/snapshots, where it reserves the trial's daily report allowance and skips proof of work. Keep this bearer credential out of URLs and logs.

Parameters
NameInTypeRequiredDescription
urlquerystring | nullnoOrigin/page URL to audit.
formatquerystring | nullno`?format=junit` → JUnit XML instead of JSON (CI-friendly). Any other value (or absent) yields JSON.
gatequeryinteger | null (int32)no`?gate=N` → a CI gate: respond `422 Unprocessable Entity` when the audit score is below `N` (so `curl --fail` blocks a deploy), else `200`.
seoGatequeryinteger | null (int32)no`?seoGate=N` → independently gate the complete technical SEO score. Missing, interrupted, or otherwise unscored SEO analysis fails closed.
expectIndexablequeryboolean | nullnoWhether this URL is intended to be indexable. Defaults to `true`; set `expectIndexable=false` for deliberate noindex/excluded pages.
sharequeryboolean | nullno`?share=true` → also publish this completed report under an unguessable id so it can be handed to someone else as a link, and return that id as `shareId`. Off by default: an automated or CI caller stores nothing. Sharing never re-runs the audit and never spends an extra credit.
Responses
StatusContentDescription
200application/json AuditReport
application/xml string
Audit completed; JUnit XML is returned when format=junit
400—URL is missing, malformed, or disallowed by the SSRF policy
402—The supplied Site Audit voucher has no paid reports remaining
403—The supplied Site Audit voucher is invalid or revoked
409—Paid credits exist but are temporarily reserved by audits in progress; retry after the Retry-After interval
422application/json AuditReport
application/xml string
The requested legacy gate or SEO gate failed
429—Free one-hour quota, per-target, or anonymous safety limit exceeded
502—Target DNS resolution failed
503—The durable Site Audit quota backend is unavailable
Example request
curl -sS 'https://api.lifub.com/audit.json'

GET /audit/crawl

Run a bounded sampled site crawlcostly
Unions sitemap, homepage-link, and optional Site Search discovery; reports per-URL provenance, strict bounded sitemap diagnostics, sampled link/site findings, and separate HTTP-health and technical SEO summaries. Coverage is always sampled and resource caps are explicit.
Parameters
NameInTypeRequiredDescription
urlquerystring | nullno
limitqueryinteger | nullnoNumber of pages to audit, clamped to `1..=MAX_PAGES`.
expectIndexablequeryboolean | nullnoWhether sampled pages are intended to be indexable. Defaults to true.
Responses
StatusContentDescription
200application/json CrawlReportBounded crawl completed
400—URL is missing, malformed, or disallowed by the SSRF policy
429—Per-target or anonymous request limit exceeded
502—Target DNS resolution failed
Example request
curl -sS 'https://api.lifub.com/audit/crawl'

GET /audit/diff

Compare two Site Audit snapshots
Returns legacy URL/health changes plus technical SEO score and stable-check changes only when the SEO analyses are comparable. Optional legacy and SEO-delta gates are independent; format=junit opts into CI output.
Parameters
NameInTypeRequiredDescription
beforequerystring | nullno
afterquerystring | nullno
formatquerystring | nullno`?format=junit` → JUnit XML instead of JSON (CI-friendly).
gatequeryinteger | null (int32)no`?gate=N` → a CI gate: respond `422` when the score delta is below `N` (default 0 → any regression fails) or anything `broke`/`disappeared`.
seoDeltaGatequeryinteger | null (int32)no`?seoDeltaGate=N` → respond `422` when comparable technical SEO score movement is below `N`; unavailable/incomparable SEO fails closed.
Responses
StatusContentDescription
200application/json SnapshotDiff
application/xml string
Snapshot diff; JUnit XML is returned when format=junit
400—Both snapshot IDs are required
404—A snapshot was not found
422application/json SnapshotDiff
application/xml string
A requested legacy or SEO delta gate failed
500—Snapshot storage failed
Example request
curl -sS 'https://api.lifub.com/audit/diff'

GET /audit/quota

Check a Site Audit voucher's remaining paid reports
Validates the X-Site-Audit-Voucher header and returns how many paid reports the voucher can still start and how many it was issued with. It runs no audit, spends no credit and needs no proof of work. When every remaining credit is reserved by audits in progress, it answers 409 with Retry-After instead of reporting zero.

Auth Requires X-Site-Audit-Voucher: <audit voucher> - Operator-issued Site Audit voucher UUID. Optional on GET /audit.json and POST /audit/snapshots, where it adds paid reports and skips the proof of work. Required on GET /audit/quota, which reports the voucher's remaining paid reports.

Responses
StatusContentDescription
200application/json VoucherQuotaStatusThe voucher's remaining and total paid reports
403—The voucher header is missing, or the voucher is invalid or revoked
409—Every remaining credit is reserved by audits in progress; retry after the Retry-After interval
429—Anonymous request limit exceeded
503—The durable Site Audit quota backend is unavailable
Example request
curl -sS 'https://api.lifub.com/audit/quota'

POST /audit/snapshots

Capture an immutable Site Audit snapshotcostly

Auth Optional X-Site-Audit-Voucher: <audit voucher> - Operator-issued Site Audit voucher UUID. Optional on GET /audit.json and POST /audit/snapshots, where it adds paid reports and skips the proof of work. Required on GET /audit/quota, which reports the voucher's remaining paid reports. or X-Site-Audit-Trial: <audit trial> - Operator-issued, host-scoped and expiring Site Audit trial. Required on GET /audit/trial and for TLS-only watch creation; optional on GET /audit.json and POST /audit/snapshots, where it reserves the trial's daily report allowance and skips proof of work. Keep this bearer credential out of URLs and logs.

Parameters
NameInTypeRequiredDescription
urlquerystring | nullno
labelquerystring | nullno
expectIndexablequeryboolean | nullnoWhether this URL is deliberately excluded from search indexing.
seoGatequeryinteger | null (int32)noOptional technical SEO score gate. Incomplete/unscored analysis fails closed and is not stored as a snapshot.
Responses
StatusContentDescription
201application/json StoredSnapshotSnapshot captured and stored
400—URL is missing, malformed, or disallowed
402—The supplied Site Audit voucher has no paid reports remaining
403—The supplied Site Audit voucher is invalid or revoked
409—Paid credits exist but are temporarily reserved by audits in progress; retry after the Retry-After interval
422application/json AuditReportThe requested technical SEO gate failed; snapshot was not stored
429—Free one-hour quota, per-target, or anonymous safety limit exceeded
500—Snapshot storage failed
503—The durable Site Audit quota backend is unavailable
Example request
curl -sS -X POST 'https://api.lifub.com/audit/snapshots'

GET /audit/snapshots/{id}

Fetch a stored Site Audit snapshot
Parameters
NameInTypeRequiredDescription
idpathstringyesSnapshot bearer-token ID
Responses
StatusContentDescription
200application/json StoredSnapshotStored snapshot
404—Snapshot not found
500—Snapshot storage failed
Example request
curl -sS 'https://api.lifub.com/audit/snapshots/{id}'

GET /audit/snapshots/{id}/suite.hurl

Download a stored Site Audit snapshot as a Hurl assertion suite
Returns the stored report as a re-runnable Hurl file for `hurl --test` or `zforce hurl`. It asserts only what the report recorded - status, final destination, content type, curated header values or presence, the page title, and a lower bound on body size - and never hashes, TLS facts, protocol or timing. Reading it re-runs nothing and spends no credit. The file never contains the snapshot ID.
Parameters
NameInTypeRequiredDescription
idpathstringyesSnapshot bearer-token ID
Responses
StatusContentDescription
200text/plain stringThe Hurl suite, as an attachment named site-audit-<host>-<yyyymmdd>.hurl
404—Snapshot not found
500—Snapshot storage or suite generation failed
Example request
curl -sS 'https://api.lifub.com/audit/snapshots/{id}/suite.hurl'

GET /audit/trial

Validate complimentary Site Audit access

Auth Requires X-Site-Audit-Trial: <audit trial> - Operator-issued, host-scoped and expiring Site Audit trial. Required on GET /audit/trial and for TLS-only watch creation; optional on GET /audit.json and POST /audit/snapshots, where it reserves the trial's daily report allowance and skips proof of work. Keep this bearer credential out of URLs and logs.

Responses
StatusContentDescription
200application/json TrialStatus
403—Invalid, revoked or expired trial
503—Trial storage unavailable
Example request
curl -sS 'https://api.lifub.com/audit/trial'

POST /audit/watches

Create a scheduled Site Audit watchcostly
Re-audits one URL on a schedule, stores every run as a snapshot, records its legacy compatibility score in a trend, and raises an alert, optionally POSTed to a webhook, when a run regresses: a score drop of at least `threshold`, a first fall below `minimumScore`, an opt-in technical-SEO drop of at least `seoThreshold`, a URL that broke or disappeared, a removed CDN, an unreachable origin, or a certificate close to expiry. The returned `id` is the watch's only credential, and there is no way to list watches. Watches never spend a Site Audit credit. Scheduled runs happen only on deployments that enable the corresponding scheduler; `POST /audit/watches/{id}/run` runs a watch on demand. Default mode `full` preserves the existing report, snapshot and score behavior. Mode `tls` requires an active domain-scoped X-Site-Audit-Trial capability, reserves one of its two TLS watches, and inspects the exact host hourly or less frequently without a full HTTP audit. Expired or revoked trial entitlement stops both scheduled and manual TLS runs.

Auth Optional X-Site-Audit-Trial: <audit trial> - Operator-issued, host-scoped and expiring Site Audit trial. Required on GET /audit/trial and for TLS-only watch creation; optional on GET /audit.json and POST /audit/snapshots, where it reserves the trial's daily report allowance and skips proof of work. Keep this bearer credential out of URLs and logs.

Parameters
NameInTypeRequiredDescription
modequerynull | WatchModenoTLS-only mode requires an active domain-scoped trial. Full is the default.
urlquerystring | nullnoThe page to watch. A bare host is audited over HTTPS; credentials are stripped, and the same SSRF and web-port checks as `/audit.json` apply.
labelquerystring | nullnoOptional free-text label, also stored on every run's snapshot.
intervalMinqueryinteger | null (int32)noMinutes between scheduled runs. Defaults to 60; clamped to 5..=1440.
thresholdqueryinteger | null (int32)noAlert when the legacy compatibility score drops by at least this many points against the previous run. Defaults to 5; clamped to 1..=100.
minimumScorequeryinteger | null (int32)noOptional absolute legacy compatibility-score floor (1..=100): alert once when a run first falls below it. Any other value is rejected.
seoThresholdqueryinteger | null (int32)noOptional technical-SEO drop threshold, clamped to 1..=100. Omitted means the technical-SEO score is not watched.
webhookquerystring | nullnoOptional URL that receives each alert as a JSON POST, which never carries the watch id. Credentials are stripped, and the SSRF and web-port checks apply.
Responses
StatusContentDescription
201application/json AuditWatchWatch created
400—The URL, mode, webhook or minimumScore is missing, malformed, or disallowed
403—The TLS trial capability is invalid, expired, revoked, or does not authorize this host
429—The global watch capacity or the anonymous request limit is exhausted
500—Watch storage failed
503—The TLS trial feature or its durable entitlement backend is unavailable
Example request
curl -sS -X POST 'https://api.lifub.com/audit/watches'

GET /audit/watches/{id}

Fetch a Site Audit watch
Returns the watch's settings and the outcome of its last run: `lastScore`, `lastRunAt`, and the `baselineSnapshotId` the next run is compared against, which `GET /audit/snapshots/{id}` reads.
Parameters
NameInTypeRequiredDescription
idpathstringyesWatch bearer-token ID
Responses
StatusContentDescription
200application/json AuditWatchThe watch
404—Watch not found
429—Anonymous request limit exceeded
500—Watch storage failed
Example request
curl -sS 'https://api.lifub.com/audit/watches/{id}'

DELETE /audit/watches/{id}

Delete a Site Audit watchlive write
Stops the watch and removes its trend and alerts. Snapshots of its earlier runs stay readable until they expire.
Parameters
NameInTypeRequiredDescription
idpathstringyesWatch bearer-token ID
Responses
StatusContentDescription
204—Watch deleted
404—Watch not found
429—Anonymous request limit exceeded
500—Watch storage failed
Example request
curl -sS -X DELETE 'https://api.lifub.com/audit/watches/{id}'

GET /audit/watches/{id}/alerts

Read a Site Audit watch's regression alerts
The newest 100 alerts at most, newest first. An unknown, deleted or expired watch reads as an empty list, not 404.
Parameters
NameInTypeRequiredDescription
idpathstringyesWatch bearer-token ID
Responses
StatusContentDescription
200application/json array<WatchAlert>Alerts, newest first
429—Anonymous request limit exceeded
500—Watch storage failed
Example request
curl -sS 'https://api.lifub.com/audit/watches/{id}/alerts'

GET /audit/watches/{id}/notifications/email

Read a watch's notification email status
Parameters
NameInTypeRequiredDescription
idpathstringyesWatch bearer-token ID
Responses
StatusContentDescription
200application/json EmailNotificationStatusRecipient confirmation and latest delivery receipt
403—Trial expired or revoked
404—Unknown watch
503—Notification store unavailable
Example request
curl -sS 'https://api.lifub.com/audit/watches/{id}/notifications/email'

POST /audit/watches/{id}/notifications/email

Request recipient confirmation for watch alertscostly
Sends a 15-minute verification UUID to the requested recipient. Replacing the address disables prior alerts immediately. The response distinguishes SMTP acceptance from a durable queued retry and never returns the verification token.
Parameters
NameInTypeRequiredDescription
idpathstringyesWatch bearer-token ID
Request body — application/json required
SchemaStartEmailRequest
Example
{
  "email": "string"
}
Responses
StatusContentDescription
202—Verification accepted or queued
400—Invalid recipient or body
403—Trial expired or revoked
404—Unknown watch
409—Notification configuration changed
429—Verification send limit
503—Delivery or store unavailable
Example request
curl -sS -X POST 'https://api.lifub.com/audit/watches/{id}/notifications/email'

DELETE /audit/watches/{id}/notifications/email

Disable a watch's email notificationslive write
Parameters
NameInTypeRequiredDescription
idpathstringyesWatch bearer-token ID
Responses
StatusContentDescription
204—Email channel removed; queued delivery will be cancelled
404—Unknown watch
503—Store unavailable
Example request
curl -sS -X DELETE 'https://api.lifub.com/audit/watches/{id}/notifications/email'

POST /audit/watches/{id}/notifications/email/verify

Confirm a watch's notification recipientcostly
Parameters
NameInTypeRequiredDescription
idpathstringyesWatch bearer-token ID
Request body — application/json required
SchemaVerifyEmailRequest
Example
{
  "token": "string"
}
Responses
StatusContentDescription
200application/json EmailNotificationStatusRecipient confirmed; token consumed
400—Invalid, expired or consumed token
403—Trial expired or revoked
404—Unknown watch
429—Confirmation attempt limit
503—Store unavailable
Example request
curl -sS -X POST 'https://api.lifub.com/audit/watches/{id}/notifications/email/verify'

POST /audit/watches/{id}/run

Run a Site Audit watch nowcostly
Audits the watched URL immediately, stores the run as a snapshot, compares it with the previous run, records any alert and POSTs it to the webhook, then returns the updated watch. An audit that fails is recorded as an `unreachable` alert, not returned as an error. The run spends no Site Audit credit.
Parameters
NameInTypeRequiredDescription
idpathstringyesWatch bearer-token ID
Responses
StatusContentDescription
200application/json AuditWatchThe watch after the run
403—The TLS trial entitlement is invalid, expired, revoked, or does not authorize this host
404—Watch not found, or deleted while the run was in flight
429—Anonymous request limit exceeded
500—Watch storage failed
503—The TLS trial feature or its durable entitlement backend is unavailable
Example request
curl -sS -X POST 'https://api.lifub.com/audit/watches/{id}/run'

GET /audit/watches/{id}/tls

Read a watch's latest TLS inspection
Returns certificate dates, trust and inspection scope from the latest completed run, or null before the first TLS observation. It never performs a new audit or sends a notification.
Parameters
NameInTypeRequiredDescription
idpathstringyesWatch bearer-token ID
Responses
StatusContentDescription
200application/json null | TlsWatchStatusLatest TLS observation, or null
404—Watch not found
429—Anonymous request limit exceeded
500—Watch storage failed
Example request
curl -sS 'https://api.lifub.com/audit/watches/{id}/tls'

GET /audit/watches/{id}/trend

Read a Site Audit watch's score trend
The newest 2000 runs at most, oldest first, one legacy compatibility score each. An unknown, deleted or expired watch reads as an empty list, not 404.
Parameters
NameInTypeRequiredDescription
idpathstringyesWatch bearer-token ID
Responses
StatusContentDescription
200application/json array<TrendSample>Score samples, oldest first
429—Anonymous request limit exceeded
500—Watch storage failed
Example request
curl -sS 'https://api.lifub.com/audit/watches/{id}/trend'

test

Test endpoints

GET /test

Delayed response test endpoint
Returns a response after an optional delay. Useful for testing timeouts, latency handling, and async behavior. Accepts any HTTP method.
Parameters
NameInTypeRequiredDescription
timeoutInMsqueryinteger | null (int64)noDelay in milliseconds before responding (defaults to 0)
Responses
StatusContentDescription
200text/plain stringTriggered response after delay
Example request
curl -sS 'https://api.lifub.com/test'

stats

Build and deployment information

GET /stats.json

Build + health info
Returns api status, version, build hash, build number, and uptime. No per-client data. For per-key usage, see the authenticated metering APIs.
Responses
StatusContentDescription
200application/json StatsBuild and health info
Example request
curl -sS 'https://api.lifub.com/stats.json'

crawler

Web crawler and site indexing API

POST /sites/crawl

Handler for POST /sites/crawl (admin multi-site crawl)costly

Auth Requires Authorization: Bearer <admin secret> - The operator's admin secret.

Parameters
NameInTypeRequiredDescription
allSitesCrawlquerybooleanyesForce crawl all sites regardless of last crawl time
isThrottledquerybooleanyesApply rate limiting
clearIndexquerybooleanyesClear index before crawl
Request body — application/json required
SchemaSitesCrawlStatus
Example
{
  "sites": [
    {
      "crawled": "string",
      "pageCount": 0,
      "siteId": "00000000-0000-4000-8000-000000000000",
      "siteProfile": {
        "configs": [
          {}
        ],
        "email": "string",
        "id": "00000000-0000-4000-8000-000000000000",
        "secret": "00000000-0000-4000-8000-000000000000"
      }
    }
  ]
}
Responses
StatusContentDescription
200application/json SitesCrawlStatusMulti-site crawl completed
401—Unauthorized - invalid service secret
Example request
curl -sS -X POST 'https://api.lifub.com/sites/crawl?allSitesCrawl={allSitesCrawl}&isThrottled={isThrottled}&clearIndex={clearIndex}'

GET /sites/crawl/status

Handler for GET /sites/crawl/status

Auth Requires Authorization: Bearer <admin secret> - The operator's admin secret.

Responses
StatusContentDescription
200application/json SitesCrawlStatusCrawl status retrieved
401—Unauthorized - invalid service secret
Example request
curl -sS 'https://api.lifub.com/sites/crawl/status'

PUT /sites/crawl/status

Handler for PUT /sites/crawl/statuslive write

Auth Requires Authorization: Bearer <admin secret> - The operator's admin secret.

Request body — application/json required
SchemaSitesCrawlStatus
Example
{
  "sites": [
    {
      "crawled": "string",
      "pageCount": 0,
      "siteId": "00000000-0000-4000-8000-000000000000",
      "siteProfile": {
        "configs": [
          {}
        ],
        "email": "string",
        "id": "00000000-0000-4000-8000-000000000000",
        "secret": "00000000-0000-4000-8000-000000000000"
      }
    }
  ]
}
Responses
StatusContentDescription
200application/json SitesCrawlStatusCrawl status updated
401—Unauthorized - invalid service secret
Example request
curl -sS -X PUT 'https://api.lifub.com/sites/crawl/status'

POST /sites/{siteId}/crawl

Handler for POST /sites/{siteId}/crawlcostly

Auth Requires Authorization: Bearer <site secret> - The site's secret UUID, issued when the site was created.

Parameters
NameInTypeRequiredDescription
siteIdpathstring (uuid)yesSite ID
urlquerystringyesURL to crawl
emailquerystringnoEmail for notifications
sitemapsOnlyquerybooleanyesOnly crawl URLs from sitemap
allowUrlWithQueryquerybooleanyesAllow URLs with query parameters
pageBodyCssSelectorquerystringyesCSS selector for page body extraction
maxPagesqueryintegernoCap pages crawled this run; clamped to the 500 throttled ceiling (only lowers it)
Responses
StatusContentDescription
200application/json CrawlerJobResultCrawl completed successfully
404—Site not found or invalid credentials
500—Crawl failed
Example request
curl -sS -X POST 'https://api.lifub.com/sites/{siteId}/crawl?url={url}&sitemapsOnly={sitemapsOnly}&allowUrlWithQuery={allowUrlWithQuery}&pageBodyCssSelector={pageBodyCssSelector}'

POST /sites/{siteId}/recrawl

Handler for POST /sites/{siteId}/recrawlcostly

Auth Requires Authorization: Bearer <site secret> - The site's secret UUID, issued when the site was created.

Parameters
NameInTypeRequiredDescription
siteIdpathstring (uuid)yesSite ID
clearIndexquerybooleanyesClear existing index before recrawl
Responses
StatusContentDescription
200application/json CrawlerJobResultRecrawl completed successfully
404—Site not found or invalid credentials
500—Recrawl failed
Example request
curl -sS -X POST 'https://api.lifub.com/sites/{siteId}/recrawl?clearIndex={clearIndex}'

sites

Site management, search, and page operations

POST /sites

POST /sites - Create a new sitelive write
Request body — application/json
Schemanull | SiteProfileCreation
Example
{
  "configs": [
    {
      "allowUrlWithQuery": false,
      "pageBodyCssSelector": "string",
      "sitemapsOnly": false,
      "url": "string"
    }
  ],
  "email": "string"
}
Responses
StatusContentDescription
201application/json SiteCreationSite created
500—Internal server error
Example request
curl -sS -X POST 'https://api.lifub.com/sites'

PUT /sites/flush

PUT /sites/flush - commit the search index (make recent writes visible)live write

Auth Requires Authorization: Bearer <admin secret> - The operator's admin secret.

Responses
StatusContentDescription
204—Indices flushed
400—Invalid credentials
503—Flush failed
Example request
curl -sS -X PUT 'https://api.lifub.com/sites/flush'

POST /sites/rss

POST /sites/rss - Create new site and index RSS feedcostly
Parameters
NameInTypeRequiredDescription
feedUrlquerystringyesRSS feed URL
stripHtmlTagsquerybooleannoStrip HTML tags from content
Responses
StatusContentDescription
200application/json SiteIndexSummaryFeed indexed
400—Invalid feed URL or content
Example request
curl -sS -X POST 'https://api.lifub.com/sites/rss?feedUrl={feedUrl}'

GET /sites/{siteId}

GET /sites/{siteId} - Fetch all document IDs for a site
Parameters
NameInTypeRequiredDescription
siteIdpathstring (uuid)yesSite ID
Responses
StatusContentDescription
200application/json array<string>Document IDs
404—No documents found
Example request
curl -sS 'https://api.lifub.com/sites/{siteId}'

DELETE /sites/{siteId}

DELETE /sites/{siteId} - Clear all pages for a sitelive write

Auth Requires Authorization: Bearer <site secret> - The site's secret UUID, issued when the site was created.

Parameters
NameInTypeRequiredDescription
siteIdpathstring (uuid)yesSite ID
Responses
StatusContentDescription
200—Site cleared
204—No content to clear
Example request
curl -sS -X DELETE 'https://api.lifub.com/sites/{siteId}'

GET /sites/{siteId}/autocomplete

GET /sites/{siteId}/autocomplete - Autocomplete suggestions
Parameters
NameInTypeRequiredDescription
siteIdpathstring (uuid)yesSite ID
queryquerystringyesPartial search query
Responses
StatusContentDescription
200application/json AutocompleteAutocomplete suggestions
400—Invalid site ID
Example request
curl -sS 'https://api.lifub.com/sites/{siteId}/autocomplete?query={query}'

GET /sites/{siteId}/har

GET /sites/{siteId}/har - Export site pages as HAR 1.2
When real HAR data exists (from crawls after HAR capture was enabled), returns filtered real HTTP metadata. Falls back to synthetic HAR from page content when no real HAR data is available (backward compatibility).

Auth One of Authorization: Bearer <site secret> - The site's secret UUID, issued when the site was created. or Authorization: Bearer <admin secret> - The operator's admin secret.

Parameters
NameInTypeRequiredDescription
siteIdpathstring (uuid)yesSite ID
statusqueryinteger (int32)noFilter by HTTP status code
sincequerystringnoISO 8601: entries crawled at or after
beforequerystringnoISO 8601: entries crawled before
urlquerystringnoURL prefix filter
minTimequerynumber (double)noMinimum response time in ms
maxTimequerynumber (double)noMaximum response time in ms
contentTypequerystringnoMIME type filter
sortquerystringnoSort: time, status, crawledAt; prefix - for desc
limitqueryintegernoMax entries (default 100, max 10000)
offsetqueryintegernoSkip first N entries
Responses
StatusContentDescription
200application/jsonHAR 1.2 export of crawled pages
403—Invalid site secret
404—No pages found for site
Example request
curl -sS 'https://api.lifub.com/sites/{siteId}/har'

GET /sites/{siteId}/pages

GET /sites/{siteId}/pages - Fetch page by URL
Parameters
NameInTypeRequiredDescription
siteIdpathstring (uuid)yesSite ID
urlquerystringyesPage URL
Responses
StatusContentDescription
200application/json FetchedPagePage found
404—Page not found
Example request
curl -sS 'https://api.lifub.com/sites/{siteId}/pages?url={url}'

PUT /sites/{siteId}/pages

PUT /sites/{siteId}/pages - Add/update page in site indexlive write

Auth Requires Authorization: Bearer <site secret> - The site's secret UUID, issued when the site was created.

Parameters
NameInTypeRequiredDescription
siteIdpathstring (uuid)yesSite ID
If-Matchheaderstring | nullnoOptional ETag precondition. Omit for legacy unconditional replacement; use * to require an existing page.
Request body — application/json required
SchemaSitePageInput
Example
{
  "body": "string",
  "id": "string",
  "labels": [
    "string"
  ],
  "siteId": "00000000-0000-4000-8000-000000000000",
  "thumbnail": "string",
  "title": "string",
  "updated": "string",
  "url": "string"
}
Responses
StatusContentDescription
200application/json FetchedPagePage indexed
400—Malformed If-Match header or missing URL
404—Invalid credentials
412—If-Match did not match the current page
Example request
curl -sS -X PUT 'https://api.lifub.com/sites/{siteId}/pages'

DELETE /sites/{siteId}/pages

DELETE /sites/{siteId}/pages - Delete page by URLlive write

Auth Requires Authorization: Bearer <site secret> - The site's secret UUID, issued when the site was created.

Parameters
NameInTypeRequiredDescription
siteIdpathstring (uuid)yesSite ID
urlquerystringyesPage URL
Responses
StatusContentDescription
204—Page deleted
404—Page not found
Example request
curl -sS -X DELETE 'https://api.lifub.com/sites/{siteId}/pages?url={url}'

PUT /sites/{siteId}/pages/{pageId}

PUT /sites/{siteId}/pages/{pageId} - Update existing page by IDlive write

Auth Requires Authorization: Bearer <site secret> - The site's secret UUID, issued when the site was created.

Parameters
NameInTypeRequiredDescription
siteIdpathstring (uuid)yesSite ID
pageIdpathstringyesPage ID (SHA-256 hash, 64 characters)
If-Matchheaderstring | nullnoOptional ETag precondition. Omit for legacy unconditional replacement; use * to require an existing page.
Request body — application/json required
SchemaSitePageInput
Example
{
  "body": "string",
  "id": "string",
  "labels": [
    "string"
  ],
  "siteId": "00000000-0000-4000-8000-000000000000",
  "thumbnail": "string",
  "title": "string",
  "updated": "string",
  "url": "string"
}
Responses
StatusContentDescription
200application/json FetchedPagePage updated
400—Invalid page ID length or missing URL
404—Invalid credentials or page not found
412—If-Match did not match the current page
Example request
curl -sS -X PUT 'https://api.lifub.com/sites/{siteId}/pages/{pageId}'

DELETE /sites/{siteId}/pages/{pageId}

DELETE /sites/{siteId}/pages/{pageId} - Delete page by IDlive write

Auth Requires Authorization: Bearer <site secret> - The site's secret UUID, issued when the site was created.

Parameters
NameInTypeRequiredDescription
siteIdpathstring (uuid)yesSite ID
pageIdpathstringyesPage ID
Responses
StatusContentDescription
204—Page deleted
404—Page not found
Example request
curl -sS -X DELETE 'https://api.lifub.com/sites/{siteId}/pages/{pageId}'

PUT /sites/{siteId}/pages/{pageId}/backdate

PUT /sites/{siteId}/pages/{pageId}/backdate?updated=<rfc3339>live write
Admin-only test affordance — see [`SiteState::backdate_page`]. Overwrites a page's `updated` timestamp so the recrawl obsolete-cleanup can be tested against a live deployment. Gated by the service/admin secret (same Bearer token as `/flush`), not a per-site secret.

Auth Requires Authorization: Bearer <admin secret> - The operator's admin secret.

Parameters
NameInTypeRequiredDescription
siteIdpathstring (uuid)yesSite ID
pageIdpathstringyesPage ID
updatedquerystringyesNew RFC 3339 `updated` timestamp
Responses
StatusContentDescription
204—Page timestamp updated
403—Invalid admin credentials
404—Page not found
Example request
curl -sS -X PUT 'https://api.lifub.com/sites/{siteId}/pages/{pageId}/backdate?updated={updated}'

GET /sites/{siteId}/profile

GET /sites/{siteId}/profile - Fetch site profile

Auth One of Authorization: Bearer <site secret> - The site's secret UUID, issued when the site was created. or Authorization: Bearer <admin secret> - The operator's admin secret.

Parameters
NameInTypeRequiredDescription
siteIdpathstring (uuid)yesSite ID
Responses
StatusContentDescription
200application/json SiteProfileProfile found
404—Profile not found
Example request
curl -sS 'https://api.lifub.com/sites/{siteId}/profile'

PUT /sites/{siteId}/profile

PUT /sites/{siteId}/profile - Update site profilelive write

Auth Requires Authorization: Bearer <site secret> - The site's secret UUID, issued when the site was created.

Parameters
NameInTypeRequiredDescription
siteIdpathstring (uuid)yesSite ID
Request body — application/json required
SchemaSiteProfileUpdate
Example
{
  "configs": [
    {
      "allowUrlWithQuery": false,
      "pageBodyCssSelector": "string",
      "sitemapsOnly": false,
      "url": "string"
    }
  ],
  "email": "string",
  "secret": "00000000-0000-4000-8000-000000000000"
}
Responses
StatusContentDescription
200application/json SiteProfileProfile updated
404—Profile not found
Example request
curl -sS -X PUT 'https://api.lifub.com/sites/{siteId}/profile'

PUT /sites/{siteId}/rss

PUT /sites/{siteId}/rss - Index RSS feed into existing sitecostly

Auth Requires Authorization: Bearer <site secret> - The site's secret UUID, issued when the site was created.

Parameters
NameInTypeRequiredDescription
siteIdpathstring (uuid)yesSite ID
feedUrlquerystringyesRSS feed URL
stripHtmlTagsquerybooleannoStrip HTML tags from content
Responses
StatusContentDescription
200application/json SiteIndexSummaryFeed indexed
400—Invalid feed URL or content
404—Site not found or invalid credentials
Example request
curl -sS -X PUT 'https://api.lifub.com/sites/{siteId}/rss?feedUrl={feedUrl}'
GET /sites/{siteId}/search - Search within a site
Parameters
NameInTypeRequiredDescription
siteIdpathstring (uuid)yesSite ID
queryquerystringyesSearch query
Responses
StatusContentDescription
200application/json SearchResultSearch results
400—Invalid site ID
Example request
curl -sS 'https://api.lifub.com/sites/{siteId}/search?query={query}'

PUT /sites/{siteId}/xml

PUT /sites/{siteId}/xml - Index generic XML feed into existing sitecostly

Auth Requires Authorization: Bearer <site secret> - The site's secret UUID, issued when the site was created.

Parameters
NameInTypeRequiredDescription
siteIdpathstring (uuid)yesSite ID
xmlUrlquerystringyesXML feed URL
stripHtmlTagsquerybooleannoStrip HTML tags from content
clearIndexquerybooleannoClear existing index before importing
Responses
StatusContentDescription
200application/json SiteIndexSummaryXML indexed
400—Invalid XML URL or content
404—Site not found or invalid credentials
Example request
curl -sS -X PUT 'https://api.lifub.com/sites/{siteId}/xml?xmlUrl={xmlUrl}'

email

Email notification endpoints

POST /sites/{siteId}/email/setup-info

POST /sites/{siteId}/email/setup-info — send setup information email.costly
Bearer-gated by the per-site secret. The pre-Bearer global counter that previously sat in front of this handler was removed: it incremented before credentials were checked, so any three failed requests could disable the endpoint for everyone until process restart. Authentication is the rate limit. Once auth passes, a per-site cooldown ([`EMAIL_COOLDOWN`]) prevents the endpoint from being turned into an outbound spam / mail-bomb vector for the site's owner mailbox — nobody legitimately hits "send me my setup info" more than once per minute. The cooldown is claimed *before* the send rather than after: a transport that fails slowly is exactly when a retrying client would otherwise hammer the mailbox. The address is the one stored on the site profile, never one supplied in the request, so this endpoint cannot be aimed at a third party. `email` is optional in the Getting Started Gadget, so a profile without one is a normal outcome and answers 204, not an error.

Auth Requires Authorization: Bearer <site secret> - The site's secret UUID, issued when the site was created.

Parameters
NameInTypeRequiredDescription
siteIdpathstring (uuid)yesSite ID
Responses
StatusContentDescription
200—Email sent successfully
204—No email address is registered for this site
404—Site not found or invalid credentials
429—Cooldown active — retry after Retry-After
503—Email delivery is unavailable
Example request
curl -sS -X POST 'https://api.lifub.com/sites/{siteId}/email/setup-info'

pages

Direct page lookup by ID

GET /pages/{id}

GET /pages/{id} - Fetch a page by its ID.
This endpoint allows direct page lookup without requiring site credentials. The page ID is a SHA-256 hash of the siteId + URL.
Parameters
NameInTypeRequiredDescription
idpathstringyesPage ID (SHA-256 hash)
Responses
StatusContentDescription
200application/json FetchedPagePage found
404—Page not found
Example request
curl -sS 'https://api.lifub.com/pages/{id}'

assets

Asset Manager (Beta — subject to change) — physical asset utilization & booking engine

GET /assets/asset/{assetId}

Public consumer view of an asset and its bookable windows
Parameters
NameInTypeRequiredDescription
assetIdpathstringyesShareable asset id
Responses
StatusContentDescription
200application/json AssetViewAsset with availability
404—Unknown asset
Example request
curl -sS 'https://api.lifub.com/assets/asset/{assetId}'

GET /assets/asset/{assetId}/calendar.ics

Public availability calendar for one asset as iCalendar (.ics)
Parameters
NameInTypeRequiredDescription
assetIdpathstringyesShareable asset id
Responses
StatusContentDescription
200text/calendartext/calendar
404—Unknown asset
Example request
curl -sS 'https://api.lifub.com/assets/asset/{assetId}/calendar.ics'

POST /assets/asset/{assetId}/windows/{windowId}/hold

Tentatively hold a seat that auto-releases after ttlSeconds (atomic, capacity-aware)costly
Parameters
NameInTypeRequiredDescription
assetIdpathstringyesShareable asset id
windowIdpathstringyesWindow to hold a seat on
Request body — application/json required
SchemaNewHold
Example
{
  "consumer": "string",
  "fields": {},
  "note": "string",
  "promoCode": "string",
  "quantity": 0,
  "ttlSeconds": 0
}
Responses
StatusContentDescription
201application/json ReservationHeld booking (confirm it with its id before it expires)
400—Invalid ttlSeconds
404—Unknown asset or window
409—No seats left (slot_full)
Example request
curl -sS -X POST 'https://api.lifub.com/assets/asset/{assetId}/windows/{windowId}/hold'

POST /assets/asset/{assetId}/windows/{windowId}/reserve

Reserve/book a utilization window (atomic, capacity-aware)costly
Parameters
NameInTypeRequiredDescription
assetIdpathstringyesShareable asset id
windowIdpathstringyesWindow to reserve
Request body — application/json required
SchemaNewReservation
Example
{
  "consumer": "string",
  "fields": {},
  "note": "string",
  "promoCode": "string",
  "quantity": 0
}
Responses
StatusContentDescription
201application/json ReservationBooking receipt (id doubles as the cancel capability)
404—Unknown asset or window
409—No seats left (slot_full)
Example request
curl -sS -X POST 'https://api.lifub.com/assets/asset/{assetId}/windows/{windowId}/reserve'

POST /assets/asset/{assetId}/windows/{windowId}/waitlist

Join a window's waitlist; the head is auto-promoted when a seat freescostly
Parameters
NameInTypeRequiredDescription
assetIdpathstringyesShareable asset id
windowIdpathstringyesWindow to queue for
Request body — application/json required
SchemaNewWaitlist
Example
{
  "consumer": "string",
  "fields": {},
  "note": "string",
  "quantity": 0
}
Responses
StatusContentDescription
201application/json WaitlistEntryWaitlist ticket (id is the leave capability)
404—Unknown asset or window
429—Waitlist is full
Example request
curl -sS -X POST 'https://api.lifub.com/assets/asset/{assetId}/windows/{windowId}/waitlist'

DELETE /assets/asset/{assetId}/windows/{windowId}/waitlist/{ticketId}

Leave a window's waitlist (ticketId is the capability)live write
Parameters
NameInTypeRequiredDescription
assetIdpathstringyesShareable asset id
windowIdpathstringyesWindow the ticket is on
ticketIdpathstringyesWaitlist ticket to remove
Responses
StatusContentDescription
204—Left the waitlist
404—Unknown ticket
Example request
curl -sS -X DELETE 'https://api.lifub.com/assets/asset/{assetId}/windows/{windowId}/waitlist/{ticketId}'

POST /assets/owners

Mint a new ownerId (manager bearer secret) — Betacostly
Responses
StatusContentDescription
201—Minted ownerId — `{ "ownerId": "<uuid>" }`
Example request
curl -sS -X POST 'https://api.lifub.com/assets/owners'

GET /assets/owners/{ownerId}/assets

List the owner's assets; ?windows=false for a lightweight summary

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId
windowsquerybooleannofalse → metadata + windowCount only (no inlined windows)
Responses
StatusContentDescription
200application/json AssetRowsResponseAssets (with windows, or summary); delegated responses omit ownerId and Viewer responses omit reservation IDs
404—Unknown owner
Example request
curl -sS 'https://api.lifub.com/assets/owners/{ownerId}/assets'

POST /assets/owners/{ownerId}/assets

Create an assetlive write

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId
Request body — application/json required
SchemaNewAsset
Example
{
  "bookingFields": [
    {
      "key": "string",
      "label": "string",
      "options": [
        "string"
      ],
      "required": false,
      "type": "text"
    }
  ],
  "currency": "string",
  "description": "string",
  "kind": "string",
  "name": "string",
  "policy": {
    "cancelCutoffMinutes": 0,
    "minNoticeMinutes": 0
  },
  "promoCodes": [
    {
      "amount": 0.0,
      "code": "string",
      "kind": "percent",
      "validFrom": 0,
      "validUntil": 0
    }
  ],
  "timezone": "string"
}
Responses
StatusContentDescription
201application/json AssetCreated asset
404—Unknown owner
Example request
curl -sS -X POST 'https://api.lifub.com/assets/owners/{ownerId}/assets'

PATCH /assets/owners/{ownerId}/assets/{assetId}

Edit an asset's mutable fields in place (only provided fields change)live write

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId
assetIdpathstringyesAsset to edit
Request body — application/json required
SchemaUpdateAsset
Example
{
  "bookingFields": [
    {
      "key": "string",
      "label": "string",
      "options": [
        "string"
      ],
      "required": false,
      "type": "text"
    }
  ],
  "currency": "string",
  "description": "string",
  "kind": "string",
  "name": "string",
  "policy": {
    "cancelCutoffMinutes": 0,
    "minNoticeMinutes": 0
  },
  "promoCodes": [
    {
      "amount": 0.0,
      "code": "string",
      "kind": "percent",
      "validFrom": 0,
      "validUntil": 0
    }
  ],
  "timezone": "string"
}
Responses
StatusContentDescription
200application/json AssetUpdated asset
400—Invalid field
404—Unknown asset
Example request
curl -sS -X PATCH 'https://api.lifub.com/assets/owners/{ownerId}/assets/{assetId}'

DELETE /assets/owners/{ownerId}/assets/{assetId}

Delete an asset and all its windowslive write

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId
assetIdpathstringyesAsset to delete
Responses
StatusContentDescription
204—Deleted
404—Unknown asset
Example request
curl -sS -X DELETE 'https://api.lifub.com/assets/owners/{ownerId}/assets/{assetId}'

POST /assets/owners/{ownerId}/assets/{assetId}/availability

Bulk-generate a recurring grid of utilization windowslive write

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId
assetIdpathstringyesAsset the windows belong to
Request body — application/json required
SchemaNewAvailability
Example
{
  "capacity": 0,
  "cost": 0.0,
  "firstStart": 0,
  "repeatCount": 0,
  "repeatEveryMinutes": 0,
  "slotMinutes": 0,
  "slots": 0
}
Responses
StatusContentDescription
201application/json AvailabilityResultGeneration result
400—Invalid recurrence
404—Unknown asset
Example request
curl -sS -X POST 'https://api.lifub.com/assets/owners/{ownerId}/assets/{assetId}/availability'

GET /assets/owners/{ownerId}/assets/{assetId}/windows

A page of one asset's windows (owner drill-down)

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId
assetIdpathstringyesAsset to page windows for
offsetqueryinteger (int32)noStart index (default 0)
limitqueryinteger (int32)noPage size (default 200, max 1000)
Responses
StatusContentDescription
200application/json WindowsPageResponseA page of windows; Viewer responses omit reservation IDs
404—Unknown asset
Example request
curl -sS 'https://api.lifub.com/assets/owners/{ownerId}/assets/{assetId}/windows'

POST /assets/owners/{ownerId}/assets/{assetId}/windows

Add a utilization window (time slot with a cost + capacity)live write

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId
assetIdpathstringyesAsset the window belongs to
Request body — application/json required
SchemaNewWindow
Example
{
  "capacity": 0,
  "cost": 0.0,
  "end": 0,
  "start": 0
}
Responses
StatusContentDescription
201application/json WindowCreated window
404—Unknown asset
409—Window overlaps an existing one
Example request
curl -sS -X POST 'https://api.lifub.com/assets/owners/{ownerId}/assets/{assetId}/windows'

DELETE /assets/owners/{ownerId}/assets/{assetId}/windows

Bulk-delete an asset's windows whose start is in [from, to) (clear a date range)live write

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId
assetIdpathstringyesAsset to clear windows on
fromqueryinteger (int64)yesRange start, epoch-ms (inclusive)
toqueryinteger (int64)yesRange end, epoch-ms (exclusive)
Responses
StatusContentDescription
200application/json BulkDeleteResultCount deleted
400—Missing/invalid range
404—Unknown asset
Example request
curl -sS -X DELETE 'https://api.lifub.com/assets/owners/{ownerId}/assets/{assetId}/windows?from={from}&to={to}'

PATCH /assets/owners/{ownerId}/assets/{assetId}/windows/{windowId}

Edit a window's cost/capacity in place (time range immutable)live write

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId
assetIdpathstringyesAsset the window belongs to
windowIdpathstringyesWindow to edit
Request body — application/json required
SchemaUpdateWindow
Example
{
  "capacity": 0,
  "cost": 0.0
}
Responses
StatusContentDescription
200application/json WindowUpdated window
400—Invalid cost/capacity
404—Unknown asset or window
409—Capacity below current bookings
Example request
curl -sS -X PATCH 'https://api.lifub.com/assets/owners/{ownerId}/assets/{assetId}/windows/{windowId}'

DELETE /assets/owners/{ownerId}/assets/{assetId}/windows/{windowId}

Delete a utilization windowlive write

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId
assetIdpathstringyesAsset the window belongs to
windowIdpathstringyesWindow to delete
Responses
StatusContentDescription
204—Deleted
404—Unknown asset or window
Example request
curl -sS -X DELETE 'https://api.lifub.com/assets/owners/{ownerId}/assets/{assetId}/windows/{windowId}'

GET /assets/owners/{ownerId}/audit

The owner's activity log (audit trail), newest first

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId
Responses
StatusContentDescription
200application/json AuditRowsResponseAudit events; delegated responses omit reservationId and free-form detail
404—Unknown owner
Example request
curl -sS 'https://api.lifub.com/assets/owners/{ownerId}/audit'

GET /assets/owners/{ownerId}/calendar.ics

Owner's full schedule as an iCalendar (.ics) feed

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId
Responses
StatusContentDescription
200text/calendartext/calendar
404—Unknown owner
Example request
curl -sS 'https://api.lifub.com/assets/owners/{ownerId}/calendar.ics'

GET /assets/owners/{ownerId}/export

Export the owner's full data snapshot (assets, bookings, members, webhooks) — owner only

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId (root)
Responses
StatusContentDescription
200application/json OwnerExportPortable snapshot
403—Only the owner may export
404—Unknown owner
Example request
curl -sS 'https://api.lifub.com/assets/owners/{ownerId}/export'

POST /assets/owners/{ownerId}/import

Import a data snapshot into an empty owner (ids preserved) — owner onlylive write

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesA freshly-minted, empty ownerId (root)
Request body — application/json required
SchemaOwnerExport
Example
{
  "assets": [
    {
      "asset": {
        "bookingFields": [
          {}
        ],
        "createdAt": 0,
        "currency": "string",
        "description": "string",
        "id": "string",
        "kind": "string",
        "name": "string",
        "ownerId": "string",
        "policy": {},
        "promoCodes": [
          {}
        ],
        "timezone": "string"
      },
      "windowCount": 0,
      "windows": [
        {
          "assetId": "string",
          "capacity": 0,
          "cost": 0.0,
          "end": 0,
          "id": "string",
          "remaining": 0,
          "reservations": [
            {}
          ],
          "start": 0,
          "waitlisted": 0
        }
      ]
    }
  ],
  "exportedAt": 0,
  "members": [
    {
      "createdAt": 0,
      "id": "string",
      "label": "string",
      "role": "owner"
    }
  ],
  "ownerId": "string",
  "version": 0,
  "webhooks": [
    {
      "createdAt": 0,
      "events": [
        "string"
      ],
      "id": "string",
      "url": "string"
    }
  ]
}
Responses
StatusContentDescription
201application/json ImportResultImport counts
400—Invalid snapshot or unsupported export version
403—Only the owner may import
404—Unknown owner
409—Target owner is not empty or a globally keyed id collides
Example request
curl -sS -X POST 'https://api.lifub.com/assets/owners/{ownerId}/import'

GET /assets/owners/{ownerId}/members

List the owner's role-scoped member tokens (owner only)

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId (root)
Responses
StatusContentDescription
200application/json array<Member>Members
403—Only the owner may list members
404—Unknown owner
Example request
curl -sS 'https://api.lifub.com/assets/owners/{ownerId}/members'

POST /assets/owners/{ownerId}/members

Mint a role-scoped member token (owner only)live write

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId (root)
Request body — application/json required
SchemaNewMember
Example
{
  "label": "string",
  "role": "string"
}
Responses
StatusContentDescription
201application/json MemberMinted member token (id is the bearer token + revoke handle)
400—Invalid role/label
403—Only the owner may mint members
404—Unknown owner
429—Per-owner member limit reached
Example request
curl -sS -X POST 'https://api.lifub.com/assets/owners/{ownerId}/members'

DELETE /assets/owners/{ownerId}/members/{memberId}

Revoke a member token (owner only)live write

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId (root)
memberIdpathstringyesMember token to revoke
Responses
StatusContentDescription
204—Revoked
403—Only the owner may revoke members
404—Unknown owner or member
Example request
curl -sS -X DELETE 'https://api.lifub.com/assets/owners/{ownerId}/members/{memberId}'

GET /assets/owners/{ownerId}/report

Owner utilization + revenue report (aggregation over all assets)

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId
Responses
StatusContentDescription
200application/json OwnerReportAggregated report
404—Unknown owner
Example request
curl -sS 'https://api.lifub.com/assets/owners/{ownerId}/report'

GET /assets/owners/{ownerId}/reservations

List active bookings across the owner's assets (paginated)

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId
offsetqueryinteger (int32)noStart index (default 0)
limitqueryinteger (int32)noPage size (default 200, max 1000)
Responses
StatusContentDescription
200application/json ReservationRowsResponseReservations (page); Viewer responses omit reservation IDs
404—Unknown owner
Example request
curl -sS 'https://api.lifub.com/assets/owners/{ownerId}/reservations'

POST /assets/owners/{ownerId}/reservations/{reservationId}/cancel

Owner cancels a booking on one of their assets (frees the seat)live write

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId
reservationIdpathstringyesBooking to cancel
Responses
StatusContentDescription
200application/json ReservationCancelled booking
404—Unknown reservation
Example request
curl -sS -X POST 'https://api.lifub.com/assets/owners/{ownerId}/reservations/{reservationId}/cancel'

PATCH /assets/owners/{ownerId}/reservations/{reservationId}/modify

Owner edits a booking on one of their assets in place (seat quantity, fields, note)live write

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId (or a member token)
reservationIdpathstringyesBooking to modify
Request body — application/json required
SchemaModifyReservation
Example
{
  "fields": {},
  "note": "string",
  "quantity": 0
}
Responses
StatusContentDescription
200application/json ReservationUpdated booking
400—Invalid quantity/fields/note
404—Unknown reservation
409—Increase doesn't fit (slot_full) / booking not active
Example request
curl -sS -X PATCH 'https://api.lifub.com/assets/owners/{ownerId}/reservations/{reservationId}/modify'

GET /assets/owners/{ownerId}/webhooks

List the owner's registered outbound webhooks

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId
Responses
StatusContentDescription
200application/json array<Webhook>Registered webhooks
404—Unknown owner
Example request
curl -sS 'https://api.lifub.com/assets/owners/{ownerId}/webhooks'

POST /assets/owners/{ownerId}/webhooks

Register an outbound event webhook (SSRF-guarded, web ports only)live write

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId
Request body — application/json required
SchemaNewWebhook
Example
{
  "events": [
    "string"
  ],
  "url": "string"
}
Responses
StatusContentDescription
201application/json WebhookRegistered webhook (id doubles as the delete capability)
400—Invalid or disallowed URL / event
404—Unknown owner
429—Per-owner webhook limit reached
Example request
curl -sS -X POST 'https://api.lifub.com/assets/owners/{ownerId}/webhooks'

DELETE /assets/owners/{ownerId}/webhooks/{webhookId}

Delete a registered webhooklive write

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesManager's secret ownerId
webhookIdpathstringyesWebhook to delete
Responses
StatusContentDescription
204—Deleted
404—Unknown owner or webhook
Example request
curl -sS -X DELETE 'https://api.lifub.com/assets/owners/{ownerId}/webhooks/{webhookId}'

GET /assets/owners/{ownerId}/whoami

Resolve the caller's effective role (owner or a member token)

Auth The ownerId path segment is the owner's bearer secret; treat the URL as a credential.

Parameters
NameInTypeRequiredDescription
ownerIdpathstringyesOwner secret or a member token
Responses
StatusContentDescription
200application/json WhoAmIResolved identity
404—Unknown owner/member token
Example request
curl -sS 'https://api.lifub.com/assets/owners/{ownerId}/whoami'

POST /assets/reservations/{reservationId}/cancel

Consumer cancels their own booking (reservationId is the capability)costly
Parameters
NameInTypeRequiredDescription
reservationIdpathstringyesThe booking receipt id
Responses
StatusContentDescription
200application/json ReservationCancelled booking
404—Unknown reservation
Example request
curl -sS -X POST 'https://api.lifub.com/assets/reservations/{reservationId}/cancel'

POST /assets/reservations/{reservationId}/confirm

Promote a held seat to a confirmed booking (reservationId is the capability)costly
Parameters
NameInTypeRequiredDescription
reservationIdpathstringyesThe held booking's receipt id
Responses
StatusContentDescription
200application/json ReservationConfirmed booking
404—Unknown reservation
409—Hold already expired, or the booking isn't a hold
Example request
curl -sS -X POST 'https://api.lifub.com/assets/reservations/{reservationId}/confirm'

PATCH /assets/reservations/{reservationId}/modify

Consumer edits their active booking in place (seat quantity, custom fields, note)costly
Parameters
NameInTypeRequiredDescription
reservationIdpathstringyesThe booking receipt id
Request body — application/json required
SchemaModifyReservation
Example
{
  "fields": {},
  "note": "string",
  "quantity": 0
}
Responses
StatusContentDescription
200application/json ReservationUpdated booking
400—Invalid quantity/fields/note
404—Unknown reservation
409—Increase doesn't fit (slot_full) / booking not active
Example request
curl -sS -X PATCH 'https://api.lifub.com/assets/reservations/{reservationId}/modify'

POST /assets/reservations/{reservationId}/reschedule

Consumer reschedules their booking onto another window of the same assetcostly
Parameters
NameInTypeRequiredDescription
reservationIdpathstringyesThe booking receipt id
Request body — application/json required
SchemaRescheduleRequest
Example
{
  "windowId": "string"
}
Responses
StatusContentDescription
201application/json ReservationNew booking (fresh reservationId)
404—Unknown reservation or target window
409—Target full / already cancelled / same window
Example request
curl -sS -X POST 'https://api.lifub.com/assets/reservations/{reservationId}/reschedule'

visit

Visit tracking beacon, Web Vitals, engagement, and per-site analytics

GET /analytics.js

Consolidated tracking tag (server record + client collector)live write
Fetched by every client that loads the embed. Records the visit from server-side request fingerprints (IP / JA3 / JA4 / TLS cipher / HTTP protocol / UA / ASN / country) and returns a small collector script that enriches the row with client signals when executed. `siteId` is required to count the visit.
Parameters
NameInTypeRequiredDescription
siteIdquerystringnoSite id to count the visit against
queryIPquerystringnoOverride IP for the geolocation / ASN lookup
cookiesquerybooleannoOption B: enable the first-party loxal_vid cookie (returning-visitor detection)
bannerquerybooleannoOption B + banner: serve Analytics' own equal-prominence consent UI
cookieConsentquerystringnoOption B2 (`signal`): defer the cookie write until `window.loxalAnalytics.consent('granted')`
cmpquerystringnoCompatibility alias: `external` selects `cookieConsent=signal` (requires `cookies=true`)
vitalsquerybooleannoOpt-in Core Web Vitals collection (LCP / INP / CLS / TTFB), posted to /visit/vitals.json on visibilitychange
engagementquerybooleannoOpt-in engagement-depth collection (time on page, scroll %, interaction count), posted to /visit/engagement.json on visibilitychange
outboundquerybooleannoOpt-in outbound-clicks & downloads collection (external <a> hosts, download file extensions), posted to /visit/outbound.json on visibilitychange
Responses
StatusContentDescription
200—Collector JavaScript (text/javascript)
Example request
curl -sS 'https://api.lifub.com/analytics.js'

POST /visit.json

Visit beacon with bot / AI-agent recognitionlive write
First-party analytics beacon: the tracking tag POSTs the visitor's client runtime signals (all optional) and this fuses them with the server-side IP / ASN / header fraud analysis (shared with /inspect.json) to return a human / suspect / bot verdict for the visit. Anonymous callers are rate-limited per IP; keyed callers are metered against their plan quota — any tier may call it. A missing or malformed body still returns a server-side verdict (a beacon must never hard-fail the host page). Optional `queryIP` overrides the IP used for the geolocation / ASN lookup, mirroring /whois.json and /inspect.json.

Auth Optional X-API-Key: <API key> - Optional API key. Anonymous callers are rate-limited per IP; a key raises the limit and meters usage.

Parameters
NameInTypeRequiredDescription
queryIPquerystringnoOverride IP for the geolocation / ASN lookup
Request body — application/json required
DescriptionClient runtime signals from the tracking tag (all optional)
SchemaClientSignals
Example
{
  "hardwareConcurrency": null,
  "languagesCount": null,
  "pluginsCount": null,
  "referrer": null,
  "url": null,
  "webdriver": null
}
Responses
StatusContentDescription
200application/json VisitVisit verdict
429—Rate limit or monthly quota exceeded
500—Internal server error
Example request
curl -sS -X POST 'https://api.lifub.com/visit.json'

POST /visit/engagement.json

Attach engagement depth to a /analytics.js visitlive write
Third step of the /analytics.js tag (JS clients, `?engagement=true` only). POSTs the visit's visible time-on-page, deepest scroll %, and interaction count as a small JSON object; the server merges them into the visit's `meta.engagement`. Never affects the bot / human verdict.

Auth Optional X-API-Key: <API key> - Optional API key. Anonymous callers are rate-limited per IP; a key raises the limit and meters usage.

Parameters
NameInTypeRequiredDescription
idquerystringyesvisit_id issued by GET /analytics.js
Request body — application/json required
DescriptionEngagement-depth sample
SchemaPageEngagement
Example
{
  "interactions": null,
  "scrollPct": null,
  "timeOnPageMs": null
}
Responses
StatusContentDescription
200—Accepted (fire-and-forget)
Example request
curl -sS -X POST 'https://api.lifub.com/visit/engagement.json?id={id}'

POST /visit/enrich.json

Enrich a /analytics.js visit with client signalslive write
Second step of the /analytics.js tag (JS clients only). POSTs client runtime signals for the `id` returned by /analytics.js; re-fuses them with the stored server verdict, updates the row, and returns the verdict as JSON.

Auth Optional X-API-Key: <API key> - Optional API key. Anonymous callers are rate-limited per IP; a key raises the limit and meters usage.

Parameters
NameInTypeRequiredDescription
idquerystringyesvisit_id issued by GET /analytics.js
Request body — application/json required
DescriptionClient runtime signals
SchemaClientSignals
Example
{
  "hardwareConcurrency": null,
  "languagesCount": null,
  "pluginsCount": null,
  "referrer": null,
  "url": null,
  "webdriver": null
}
Responses
StatusContentDescription
200—Verdict (verdict / score / reasons) as JSON
Example request
curl -sS -X POST 'https://api.lifub.com/visit/enrich.json?id={id}'

POST /visit/outbound.json

Attach outbound clicks & downloads to a /analytics.js visitlive write
Fourth step of the /analytics.js tag (JS clients, `?outbound=true` only). POSTs a batch of outbound clicks / downloads for the visit (destination hosts and/or download file extensions only — never the raw href, path, query, or fragment); the server merges them into the visit's `meta.outbound`. Never affects the bot / human verdict.

Auth Optional X-API-Key: <API key> - Optional API key. Anonymous callers are rate-limited per IP; a key raises the limit and meters usage.

Parameters
NameInTypeRequiredDescription
idquerystringyesvisit_id issued by GET /analytics.js
Request body — application/json required
DescriptionBatched outbound events
SchemaPageOutbound
Example
{
  "events": [
    {
      "downloadExt": null,
      "host": null
    }
  ]
}
Responses
StatusContentDescription
200—Accepted (fire-and-forget)
Example request
curl -sS -X POST 'https://api.lifub.com/visit/outbound.json?id={id}'

POST /visit/retention.json

Set a site's data-retention TTL (days) — requires siteSecretlive write

Auth Requires ?siteSecret=<site secret> - The site's secret UUID, as a query parameter.

Parameters
NameInTypeRequiredDescription
siteIdquerystringyesSite id (UUID) to configure
siteSecretquerystringyesThe site's write/config secret (UUID)
daysqueryinteger (int64)yesRetention in days (>= 1); default 365
Responses
StatusContentDescription
200—Updated retention
400—Missing/invalid siteId or days
401—Missing or invalid siteSecret
503—Metering backend temporarily unavailable — retry later (Retry-After)
Example request
curl -sS -X POST 'https://api.lifub.com/visit/retention.json?siteId={siteId}&siteSecret={siteSecret}&days={days}'

GET /visit/stats.json

Aggregated visit analytics for a site
Returns visit totals (human / suspect / bot), a per-UTC-day human/bot time series, and top-pages / top-referrers breakdowns for a tracked `siteId`, over the last `days` days (default 7, max 365). `siteId` is a required UUID — the unguessable id is itself the read capability, so no separate token is needed for public sites; a non-UUID `siteId` is rejected. Private sites require a valid `siteSecret`. A verified owner's response additionally includes the current `retentionDays`; the field is absent when ownership or retention cannot be verified.

Auth Optional X-API-Key: <API key> - Optional API key. Anonymous callers are rate-limited per IP; a key raises the limit and meters usage.

Parameters
NameInTypeRequiredDescription
siteIdquerystringyesSite id (UUID) whose analytics to read
daysqueryinteger (int32)noWindow length in days (default 7, max 365)
siteSecretquerystringnoOwner secret; required for private analytics, otherwise optional for retention readback. Reading never claims an unowned site.
Responses
StatusContentDescription
200application/json VisitStatsResponseAggregated visit analytics with owner-only retention when available
400—Missing or non-UUID `siteId` parameter
401—Private analytics require a valid siteSecret
500—Internal server error
503—Metering backend unavailable or report capacity busy - retry later (Retry-After: 30)
Example request
curl -sS 'https://api.lifub.com/visit/stats.json?siteId={siteId}'

POST /visit/visibility.json

Toggle a site's analytics public/private — requires siteSecretlive write

Auth Requires ?siteSecret=<site secret> - The site's secret UUID, as a query parameter.

Parameters
NameInTypeRequiredDescription
siteIdquerystringyesSite id (UUID) to configure
siteSecretquerystringyesThe site's write/config secret (UUID)
privatequerybooleanyestrue = private (stats reads need the siteSecret); false = public
Responses
StatusContentDescription
200—Updated visibility
400—Missing/invalid siteId or private flag
401—Missing or invalid siteSecret
503—Metering backend temporarily unavailable — retry later (Retry-After)
Example request
curl -sS -X POST 'https://api.lifub.com/visit/visibility.json?siteId={siteId}&siteSecret={siteSecret}&private={private}'

POST /visit/vitals.json

Attach Core Web Vitals to a /analytics.js visitlive write
Third step of the /analytics.js tag (JS clients, `?vitals=true` only). POSTs the visit's Largest Contentful Paint, Interaction to Next Paint, Cumulative Layout Shift, and Time to First Byte as a small JSON object; the server merges them into the visit's `meta.vitals`. Never affects the bot / human verdict.

Auth Optional X-API-Key: <API key> - Optional API key. Anonymous callers are rate-limited per IP; a key raises the limit and meters usage.

Parameters
NameInTypeRequiredDescription
idquerystringyesvisit_id issued by GET /analytics.js
Request body — application/json required
DescriptionCore Web Vitals sample
SchemaPageVitals
Example
{
  "cls": null,
  "inpMs": null,
  "lcpMs": null,
  "ttfbMs": null
}
Responses
StatusContentDescription
200—Accepted (fire-and-forget)
Example request
curl -sS -X POST 'https://api.lifub.com/visit/vitals.json?id={id}'

admin

Operator-only crawler introspection

GET /admin/active-crawls

Handler for GET /admin/active-crawls
Returns the number of in-flight crawls plus the site IDs currently being crawled. Used by the §7.4 migration runbook to poll for `running == 0` after setting `MIGRATION_LOCKDOWN=true`.

Auth Requires Authorization: Bearer <admin secret> - The operator's admin secret.

Responses
StatusContentDescription
200application/json ActiveCrawlsResponseActive crawl counter
401—Unauthorized - invalid admin secret
Example request
curl -sS 'https://api.lifub.com/admin/active-crawls'

GET /admin/promoted

Handler for GET /admin/promoted
Reports whether the embedded store's warm-standby writer has been armed (`EmbeddedStore::promote`, see the `api` Deployment `strategy:` comment in `api.yaml`). Unauthenticated by design — a pure operational liveness signal like `/health`, exposing no data. A warm-standby process answers `/health` while still read-only, before it takes the SQLite writer flock. Deployment checks can poll this endpoint so callers never race the promotion window into a raw 500 "attempt to write a readonly database" (`voyk/src/store/embedded/mod.rs::promote`).
Responses
StatusContentDescription
200—Writer promoted; the store accepts writes
503—Still warm-standby; retry after the given Retry-After
Example request
curl -sS 'https://api.lifub.com/admin/promoted'

Schemas

ActiveCrawlsResponse

Response shape for `GET /admin/active-crawls` (§7.4).

Properties
NameTypeRequiredDescription
runningintegeryes
site_idsarray<string (uuid)>yes

Used by: GET /admin/active-crawls

AnalyticsStatsSummary

Successful `visit_stats` computes, published on `/stats.json` as `analyticsStats` so the `redis-capacity` CI job can page when the compute time drifts toward STATS_COMPUTE_CEILING - the growth signal that would otherwise end in a permanent 503 (the 2026-09-02 lesson: a warning nobody reads is not detection).

Properties
NameTypeRequiredDescription
daysinteger (int64)yesWindow of the most recent successful compute, in days.
lastComputeMsinteger (int64)yesWall-clock milliseconds of the most recent successful compute.
lastOkUnixMsinteger (int64)yesUnix milliseconds when that compute finished.
recentMaxBucketMsinteger | null (int64)noResolution of the rolling maximum's fixed-size buckets.
recentMaxComputeMsinteger | null (int64)noLargest successful compute in this process's recent minute buckets.
recentMaxWindowMsinteger | null (int64)noRequested rolling window. Minute buckets conservatively retain a peak for up to one additional minute; these are per-process, not fleet stats.
sitestringyesSite of the most recent successful compute, not necessarily the recent peak.

Asset

A published asset. `ownerId` is the manager's secret and is only returned to the root manager/export surface, never to delegated or public views.

Properties
NameTypeRequiredDescription
bookingFieldsarray<BookingFieldDef>noOwner-defined fields a consumer must/may fill when booking (empty = none).
createdAtinteger (int64)yesEpoch-ms creation time.
currencystringyesCurrency the window costs are quoted in — a free-form symbol or code (`$`, `USD`, `EUR`, `£`, `CHF`, …); **not necessarily USD**.
descriptionstring | nullno
idstringyesUnguessable UUID; doubles as the shareable consumer link.
kindstringyesFree-form category, e.g. `drone`, `court`, `vehicle`, `lesson`, `property`.
namestringyesHuman name, e.g. "Court 1", "DJI Mavic 3", "Grand-piano lesson".
ownerIdstringyesThe managing owner's secret UUID.
policynull | BookingPolicyno
promoCodesarray<PromoCode>noOwner-defined discount codes (empty = none). Only ever returned on the manager surface — never leaked on the public [`AssetView`].
timezonestringnoIANA timezone the window times are meant to be read in (e.g. `Europe/Berlin`); defaults to `UTC`. Times on the wire stay epoch-ms UTC — this only drives how a client renders them.

Used by: POST /assets/owners/{ownerId}/assets, PATCH /assets/owners/{ownerId}/assets/{assetId}

AssetRowsResponse

Role-dependent serialization for `GET /owners/{token}/assets`.

Type: array<AssetWithWindows> | array<DelegatedAssetWithWindows> | array<ViewerAssetWithWindows>

Used by: GET /assets/owners/{ownerId}/assets

AssetView

The public consumer view of an asset: descriptive fields + windows, **without** the `ownerId` and without other consumers' booking details.

Properties
NameTypeRequiredDescription
bookingFieldsarray<BookingFieldDef>noCustom fields the consumer must/may fill when booking (empty = none).
currencystringyes
descriptionstring | nullno
idstringyes
kindstringyes
namestringyes
timezonestringyesIANA timezone the consumer should read the slots in (default `UTC`).
windowsarray<PublicWindow>yes

Used by: GET /assets/asset/{assetId}

AssetWithWindows

Manager view of one asset: the asset plus all its windows (with bookings). In the lightweight summary view (`?windows=false`) `windows` is empty and only `windowCount` is populated — so listing a large catalog doesn't inline every window+booking.

Properties
NameTypeRequiredDescription
assetAssetyes
windowCountinteger (int32)noTotal windows on the asset (always set; the only window info in the summary view).
windowsarray<Window>no

AuditAction

One remediation objective backed by the checks that contributed to it.

Properties
NameTypeRequiredDescription
categorystringnoReport area associated with the highest-severity contributor.
contributingCheckIdsarray<string>noStable check or finding IDs merged into this action.
evidencearray<string>noBounded observations supporting this action.
idstringnoStable remediation-family identifier, for example `header.csp`.
recommendationstringnoConcrete remediation guidance from the highest-severity contributor.
severitystringnoHighest advisory severity among the contributing checks.
titlestringnoShort action label.
whystringnoWhy this matters: the objective consequence of leaving it unresolved, never an effort, ownership, or business-impact estimate. Omitted when empty, which is how reports stored before this field existed read.

AuditCoverage

How much of the fixed one-page audit fan-out returned usable response evidence. Coverage is independent of both health scores: a complete report can faithfully describe an unhealthy origin, while consumers can use a partial report to distinguish unevaluated checks from the frozen legacy score and absence-shaped finding contract.

Properties
NameTypeRequiredDescription
auxiliaryResponseReceivedbooleannoAt least one same-origin auxiliary probe returned an HTTP response. Together with `pageResponseReceived: false`, this fact identifies differential reachability without guessing whether the differentiator was an edge policy, WAF, client fingerprint, IP, or something else.
completebooleannoEvery scheduled probe returned an HTTP response and a complete body.
completedProbesintegernoProbes that returned an HTTP response without truncation.
pageResponseReceivedbooleannoThe primary page returned an HTTP response, irrespective of its status.
totalProbesintegernoProbes scheduled for this audit.

AuditEvent

One entry in an owner's activity log (audit trail). Append-only, newest-first, capped at [`AUDIT_MAX`] and TTL'd like the rest of the owner's data.

Properties
NameTypeRequiredDescription
actionstringyesMachine-readable action: `owner.create`, `asset.create`, `asset.update`, `asset.delete`, `window.add`, `window.update`, `window.delete`, `availability.generate`, `reserve`, `hold`, `confirm`, `cancel`, `modify`, `webhook.add`, `webhook.delete`, `member.add`, `member.delete`, `waitlist.join`, `waitlist.leave`, `waitlist.promote`, `owner.import`, `window.bulk_delete`.
assetIdstring | nullno
atinteger (int64)yesEpoch-ms when the action happened.
detailstring | nullnoShort human detail (asset name, booker handle, generated counts, …).
reservationIdstring | nullno
windowIdstring | nullno

AuditProvenance

Compact provenance for a completed one-page report.

Properties
NameTypeRequiredDescription
capturedAtinteger (int64)noServer capture time in Unix epoch milliseconds.
effectiveOriginstring | nullnoEffective scheme/host/port, only when the page returned HTTP.
effectiveUrlstring | nullnoFinal URL observed after redirects, only when the page returned HTTP.
engineVersionstringnoVersion of the Site Audit engine that produced the report.
httpClientProfileIdstringnoStable request/client profile used by the fetcher.
pageEncodingnull | PageEncodingProvenanceno
reportSchemaVersioninteger (int32)noVersion of the top-level report wire schema.
sourceRevisionstring | nullnoExact deployed Git revision when a release supplied one.
vantagestring | nullnoCoarse operator-defined capture vantage, never an egress IP or hostname.

AuditReport

Top-level audit report.

Properties
NameTypeRequiredDescription
actionsarray<AuditAction>noDeterministic remediation objectives derived from existing findings and failed checks. Additive and independent of every score.
cdnCdnInfono
coverageAuditCoverageno
findingDetailsarray<FindingDetail>noThe same findings, in the same order, each carrying a stable catalogue id, a severity, and the what/why/how explanation needed to act on it (see [`explain`]). Additive: `findings` remains the established field, and a snapshot captured before this existed simply deserializes empty.
findingsarray<string>noNotable issues found, human-readable.
healthnull | HealthReportno
hoststringnoThe normalized host that was audited.
metanull | PageMetano
performancenull | PerformanceReportno
probesarray<ProbeResult>noPer-URL results, page first then well-known paths sorted.
provenancenull | AuditProvenanceno
scoreinteger (int32)noLegacy HTTP/security health score 0–100. Its established weighting and gate semantics are intentionally independent of the versioned SEO score.
securitynull | SecurityAnalysisno
seonull | SeoReportno
shareIdstring | nullnoSet when this report was published as a shareable copy (`?share=true`): the unguessable id it is retrievable under at `/audit/snapshots/{shareId}`. Absent means the report was not stored, so nothing about this run is retrievable by anyone else.
supplementalProbesarray<ProbeResult>noRequests made beside the fixed probe set, currently `/llms.txt` for the AI readiness checks, sorted by path. They are excluded from `probes`, `coverage`, the legacy `score`, `health`, the probe changes of snapshot diffs, watch alerts, and probe JUnit. The `/llms.txt` result feeds only the unweighted `ai-readiness.llms-txt` check, which, like every SEO check, is listed in a snapshot diff's `seoCheckChanges` and in SEO-gate JUnit and never fails either. Omitted when empty, which is how reports captured before they existed read.
targetstringnoThe audited URL, normalized and with any `user:pass@` credentials stripped (never the raw input — see [`run_audit`]).
tlsnull | TlsInfono
tlsInspectionnull | TlsInspectionEvidenceno
unicodeHoststring | nullnoUnicode presentation form of an IDN host. `host` remains the canonical ASCII/Punycode security and network identity.

Used by: GET /audit.json, POST /audit/snapshots

AuditRowsResponse

Role-dependent serialization for the manager audit trail.

Type: array<AuditEvent> | array<DelegatedAuditEvent>

Used by: GET /assets/owners/{ownerId}/audit

AuditWatch

A scheduled audit watch. The `id` is the bearer secret.

Properties
NameTypeRequiredDescription
baselineSnapshotIdstring | nullnoThe previous run's snapshot id - the baseline the next run diffs against.
createdAtinteger (int64)yesCreation time, epoch milliseconds.
idstringyesThe watch's bearer secret: anyone holding it can read, run or delete the watch, and there is deliberately no way to list watches.
intervalMininteger (int32)yesMinutes between scheduled runs, clamped to 5..=1440.
labelstring | nullno
lastRunAtinteger | null (int64)noWhen the last run started, epoch milliseconds.
lastScoreinteger | null (int32)noThe legacy compatibility score of the last run that produced a report.
minimumScoreinteger | null (int32)noAlert when the frozen compatibility score first falls below this floor. Optional so watches stored before absolute thresholds existed retain their exact delta-only behaviour.
modeWatchModeno
scoreDropThresholdinteger (int32)yesAlert when the score drops by at least this many points vs the last run.
seoDropThresholdinteger | null (int32)noAlert when the *technical SEO* score drops by at least this many points vs the last run. Optional and defaulted so stored watches created before SEO scoring existed keep deserializing; absent means SEO regressions are not watched, exactly the pre-SEO behaviour. Separate from `score_drop_threshold` because the two scores measure different things (legacy compatibility vs technical search readiness) and a site can regress badly in one while the other is unchanged.
trialIdstring | nullnoHashed entitlement identity, never the trial bearer capability.
urlstringyesThe audited URL, normalized and stripped of credentials.
webhookstring | nullnoWhere alerts are POSTed as JSON, stripped of credentials.

Used by: POST /audit/watches, GET /audit/watches/{id}, POST /audit/watches/{id}/run

Autocomplete

Autocomplete response. Equivalent to Kotlin `Autocomplete`.

Properties
NameTypeRequiredDescription
resultsarray<string>yes

Used by: GET /sites/{siteId}/autocomplete

AvailabilityResult

Result of a bulk `availability` generation.

Properties
NameTypeRequiredDescription
createdinteger (int32)yesWindows actually created.
skippedinteger (int32)yesCandidate windows skipped (overlapped an existing window, or hit the cap).

Used by: POST /assets/owners/{ownerId}/assets/{assetId}/availability

BookingCounts

Booking counts by (effective) lifecycle state for a [`OwnerReport`].

Properties
NameTypeRequiredDescription
cancelledinteger (int32)yes
completedinteger (int32)yes
confirmedinteger (int32)yes
expiredinteger (int32)yes
heldinteger (int32)yes

BookingFieldDef

An owner-defined field a consumer fills in at booking time — e.g. a license plate, headcount, delivery address, or a `select` from a fixed option list. Definitions live on the [`Asset`]; the consumer-supplied values live on the [`Reservation`] (`fields`). Cross-vertical and self-contained.

Properties
NameTypeRequiredDescription
keystringyesStable machine key (`[A-Za-z0-9_-]`, unique per asset) — the key under which the consumer's value is stored in `Reservation.fields`.
labelstringyesHuman-facing label the booking UI renders.
optionsarray<string>noAllowed values for a `select` field (ignored for other types).
requiredbooleannoWhether the consumer must supply a non-empty value.
typeFieldTypeno

BookingPolicy

A per-asset booking policy: how close to a slot's start a consumer may still book it, and how close they may still cancel it. `0` = no restriction; an all-zero policy is normalised to "no policy". Owner-initiated cancels always bypass the cancel cutoff.

Properties
NameTypeRequiredDescription
cancelCutoffMinutesinteger (int32)noA consumer can't cancel a booking within this many minutes of the slot start.
minNoticeMinutesinteger (int32)noA consumer can't book/hold a window starting within this many minutes of now.

One broken internal link found during the crawl.

Properties
NameTypeRequiredDescription
referencedBystringyesA page that referenced this link (the first one seen).
statusinteger | null (int32)noThe HTTP status (4xx/5xx), or `None` when the link was unreachable.
urlstringyes

BulkDeleteResult

Result of a bulk window delete.

Properties
NameTypeRequiredDescription
deletedinteger (int32)yes

Used by: DELETE /assets/owners/{ownerId}/assets/{assetId}/windows

CdnChange

A report-level CDN / edge-network transition between two snapshots. This is an *origin* property (every same-origin probe sees the same CDN), so it lives beside `score_delta` rather than in the per-URL `changes` list.

Properties
NameTypeRequiredDescription
afterstring | nullnoVendor on the AFTER snapshot.
beforestring | nullnoVendor on the BEFORE snapshot (absent when unnamed or not detected).
kindstringyes`added` (origin → now behind a CDN), `removed` (CDN no longer fingerprinted), or `changed` (different vendor, e.g. Fastly → Cloudflare).

CdnInfo

Whether — and which — CDN / edge network fronts the audited origin, inferred from response headers. Derived at fetch time and kept deliberately compact and *value-free*: the tell-tale headers (`cf-ray`, `x-amz-cf-id`, …) carry per-request-unique values, so we record only the header **names** that matched. That keeps the signal stable across re-audits (no spurious `header-changed` in the snapshot diff).

Properties
NameTypeRequiredDescription
detectedbooleannoTrue when a CDN/edge network was positively identified (a vendor signature matched, or the RFC 8586 `CDN-Loop` header was present).
evidencearray<string>noStable evidence: the response-header names that triggered the match (values omitted — see the struct docs).
vendorstring | nullnoThe identified provider (e.g. `Cloudflare`, `Fastly`, `Amazon CloudFront`) when a vendor-specific signature matched; `None` when a CDN is present but the vendor could not be named.

ChainCert

One certificate in the chain the server presented (leaf and each additional cert), summarised for the report. A compact, SSL-Labs-style chain view.

Properties
NameTypeRequiredDescription
daysRemaininginteger | null (int64)no
issuerstring | nullno
keyBitsinteger | null (int32)no
keyTypestring | nullno`EC` / `RSA` / `Ed25519`.
notAfterinteger (int64)no`notAfter`, epoch seconds.
notAfterUtcstring | nullno
notBeforeinteger | null (int64)noAdditional evidence is optional so historical chain summaries stay unknown.
notBeforeUtcstring | nullno
selfIssuedboolean | nullnoSelf-issued certificates are reported but excluded from renewal warnings.
sha256string | nullno
signatureAlgorithmstring | nullno
subjectstring | nullno
validNowboolean | nullno

ClientSignals

Client-side runtime signals POSTed by the tag. Every field is optional: a *missing* field is "unknown" and is **not** penalised, only a field that is present-and-bot-like contributes to the score (so an empty `{}` beacon scores 0 on the client side and falls back to the server-side verdict).

Properties
NameTypeRequiredDescription
hardwareConcurrencynumber | null (double)no`navigator.hardwareConcurrency` — 0 is a headless tell.
languagesCountinteger | null (int32)no`navigator.languages.length` — headless contexts often report 0.
pluginsCountinteger | null (int32)no`navigator.plugins.length` — 0 on a non-mobile UA is mildly suspect.
referrerstring | nullno`document.referrer` — the visit's traffic source. Reduced to its host and stored for the top-referrers rollup; never used for scoring.
urlstring | nullno`location.href` of the tracked page — used only to drop same-site (internal-navigation) referrers from that rollup.
webdriverboolean | nullno`navigator.webdriver` — true under automation (Selenium/Puppeteer/…).

Used by: POST /visit.json, POST /visit/enrich.json

CollectionPeriod

The dates the field data was collected over, `YYYY-MM-DD`.

Properties
NameTypeRequiredDescription
firstDatestringno
lastDatestringno

CompressionEvidence

Result of repeating the page request with `Accept-Encoding: br, gzip`. Only the response headers are read; the body is discarded unread.

Properties
NameTypeRequiredDescription
contentEncodingstring | nullnoThe response's `Content-Encoding`, absent when it sent none.
errorKindstring | nullnoTransport failure kind when no response arrived; same vocabulary as `ProbeResult.errorKind`.
evaluatedbooleannoA response arrived.
requestedEncodingsstringnoThe `Accept-Encoding` value sent, `br, gzip`.
statusinteger | null (int32)no
varyAcceptEncodingbooleannoThe response's `Vary` names `Accept-Encoding`.

CountRow

One row of a "top N" breakdown — a path (top pages) or a referrer host (top referrers) with its visitor / view split over the window.

Properties
NameTypeRequiredDescription
botinteger (int64)yes
humaninteger (int64)yes
labelstringyesThe path (top pages) or referrer host (top referrers).
uniqueVisitorsinteger (int64)yesDistinct visitors (by client IP) — the sort key for the breakdown.
visitsinteger (int64)yesPage views (total beacons) for this label.

CrawlCoverage

Honest accounting for the bounded discovery and link sample.

Properties
NameTypeRequiredDescription
homepageLinksDiscoveredintegerno
homepageLinksTruncatedbooleanno
incompletePagesintegerno
internalLinksCheckedintegerno
internalLinksDiscoveredintegerno
linkCheckLimitReachedbooleanno
pageLimitReachedbooleanno
pagesAuditedintegerno
pagesDiscoveredintegerno
pagesSelectedintegerno
pagesWithTruncatedLinksintegerno
redirectBudgetExhaustedbooleannoAt least one otherwise-safe same-host redirect was not followed because its additional request could not be admitted under the target budget.
requestedLimitintegerno
siteSearchTruncatedbooleanno
siteSearchUrlsDiscoveredintegerno
sitemapByteLimitReachedbooleanno
sitemapBytesReadintegernoRaw response bytes retained across sitemap attempts, including error responses; this is resource accounting, not valid sitemap XML size.
sitemapDocumentReferencesRejectedintegernoInvalid, out-of-scope, too-deep, or over-queue-limit document references rejected before admission. These are not discovered/skipped documents.
sitemapDocumentsAttemptedintegernoAdmitted document candidates for which an HTTP request was attempted.
sitemapDocumentsByteLimitedintegerno
sitemapDocumentsDiscoveredintegernoDistinct normalized, in-scope sitemap document candidates admitted to the traversal queue, including the two conventional optional probes.
sitemapDocumentsFailedintegernoTransport failures and every non-2xx status except a 404 from an optional conventional probe. This can overlap `fetched` for HTTP failures.
sitemapDocumentsFetchedintegernoAttempts that received an HTTP response, regardless of its status.
sitemapDocumentsInvalidintegernoFully received responses whose XML/content is malformed or unsupported.
sitemapDocumentsSkippedintegernoAdmitted document candidates left unattempted when a traversal cap was reached. Together with `attempted`, this partitions `discovered`.
sitemapDocumentsStreamInterruptedintegerno
sitemapDocumentsTruncatedintegernoResponses not read to EOF, partitioned by the next two counters.
sitemapEntriesSeenintegerno
sitemapEntryLimitReachedbooleanno
sitemapExistsbooleanno
sitemapFetchLimitReachedbooleanno
sitemapLastmodFutureintegerno
sitemapLastmodInvalidintegerno
sitemapQueueLimitReachedbooleanno
sitemapUrlsDuplicateintegerno
sitemapUrlsInvalidintegerno
sitemapUrlsOutOfScopeintegerno
sitemapUrlsValidintegerno

CrawlDiagnostic

Stable, sampled crawl-level diagnostic. Evidence is sorted, deduplicated, and capped so a link farm or malformed sitemap cannot inflate the report.

Properties
NameTypeRequiredDescription
evidencearray<string>no
idstringno
messagestringno
sampledbooleanno
severitystringno

CrawlDiagnostics

Why a crawl indexed the page count it did. Returned on [`CrawlerJobResult::diagnostics`]. Every counter is the number of URLs that ended in that outcome; `indexed` equals [`CrawlerJobResult::page_count`]. `summary` is a human-readable explanation, most useful when `indexed` is zero or every indexed page had no extractable text (a client-side-rendered site).

Properties
NameTypeRequiredDescription
depthFilteredinteger (int64)noURLs excluded because they sit deeper than the crawl's max depth (link-distance from the seed). The depth cap is an internal crawl bound, so a large count means the site's structure is deeper than the crawler follows — same invisible-drop family as `scope_filtered`. Additive field; `#[serde(default)]` keeps pre-field payloads parseable.
emptyBodyinteger (int64)yesSubset of `indexed` whose extracted body text was empty — the signal of a client-side-rendered (JavaScript SPA) page that will not produce useful search results.
fetchErrorinteger (int64)yesURLs that never produced a usable HTTP response (DNS, TLS, connection, timeout, or an empty/unreadable body).
fetchedinteger (int64)yesURLs that reached a fetch attempt and produced a terminal outcome (everything below except `robotsBlocked` and `scopeFiltered`, which are excluded before any HTTP request is made).
frontierDroppedinteger (int64)noCandidate URLs shed because the crawl frontier hit its internal bound (`FRONTIER_MAX`) — only plausible on extremely link-dense hosts. Sheds are candidates that were never evaluated, not pages that failed. Additive field; `#[serde(default)]` keeps pre-field payloads parseable.
httpErrorinteger (int64)yesFetched URLs that returned a non-2xx HTTP status (often 403 bot protection or a 5xx).
indexErrorinteger (int64)yesPages fetched successfully but rejected by the search backend on write — a server-side error, not a problem with the crawled site. `indexed` (and `pageCount`) already exclude these, so the response stays truthful about what the store actually holds.
indexedinteger (int64)yesPages stored in the index. Equals `CrawlerJobResult.pageCount`.
nonHtmlinteger (int64)yes2xx responses whose content-type was not indexable HTML/PDF.
robotsBlockedinteger (int64)yesURLs excluded before fetch because robots.txt disallows the crawler user-agent (`opty`).
samplesarray<DiagnosticSample>yes
scopeFilteredinteger (int64)noURLs excluded before fetch because they fall outside the crawl scope: the crawler only follows URLs under the SEED URL'S PATH PREFIX (`should_crawl_url`), plus extension/query filters. A large count next to a tiny `indexed` is the signature of a too-narrow seed — e.g. an article URL instead of the host root (build #2391, where this drop was invisible and cost a live bisect to find). `#[serde(default)]` keeps payloads from pre-field builds parseable.
seedRedirectedTostring | nullnoSet when the seed URL redirected and the crawl rebased its scope onto the redirect's final scheme/host/port (`engine::rebase_scope`) — the operator gave one URL and the crawl ran against another, which qualification needs to know. Additive field; `#[serde(default)]` keeps pre-field payloads parseable and `None` stays off the wire.
summarystringyes

CrawlPage

One audited page in a crawl.

Properties
NameTypeRequiredDescription
classstringyes`present` / `redirected` / `error` / `unreachable`.
completebooleannoWhether the response body reached EOF without hitting the byte cap or a stream error. `false` for unreachable pages and legacy deserializations.
contentTypestring | nullno
finalUrlstring | nullno
linkCountintegeryesSame-host links harvested from this page (for the link check).
linksTruncatedbooleannoTrue when more same-host links existed than the per-page crawl cap.
pageEncodingnull | PageEncodingProvenanceno
provenancearray<DiscoverySource>noEvery bounded discovery source which named this URL.
seonull | SeoReportno
sizeinteger | nullno
statusinteger | null (int32)no
titlestring | nullno
urlstringyes

CrawlReport

Full crawl report.

Properties
NameTypeRequiredDescription
brokenLinksarray<BrokenLink>no
completebooleannoTrue when every operation in the selected bounded sample completed and no discovery/link resource limit was reached. This never means whole-web coverage; [`sampled`] remains true.
coverageCrawlCoverageno
diagnosticsarray<CrawlDiagnostic>no
discoveredViastringyesHow the page set was discovered: `sitemap` / `sitemap-index` / `homepage-links` / `site-search`.
findingsarray<string>no
hoststringyes
pagesarray<CrawlPage>yes
pagesAuditedintegeryes
pagesDiscoveredintegeryesDistinct pages discovered before the `limit` cap.
sampledbooleanno
scoreinteger (int32)yesAggregate health: percent of audited pages returning a healthy 2xx/3xx.
seoCrawlSeoSummaryno
targetstringyes
unicodeHoststring | nullnoUnicode presentation form of an IDN host. `host` remains canonical ASCII/Punycode for network and security decisions.

Used by: GET /audit/crawl

CrawlSeoSummary

Mean SEO score over scoreable sampled pages, kept separate from the legacy crawl health score.

Properties
NameTypeRequiredDescription
completebooleanno
meanScoreinteger | null (int32)no
pagesCompleteintegerno
pagesScoredintegerno
pagesWithAnalysisintegerno
scoreModelVersioninteger | null (int32)no

CrawlStatus

Crawl status for a site. Equivalent to Kotlin `CrawlStatus`.

Properties
NameTypeRequiredDescription
crawledstringyes
pageCountinteger (int64)no
siteIdstring | null (uuid)no
siteProfilenull | SiteProfileno

CrawlerJobResult

Result of a crawl job. Equivalent to Kotlin `CrawlerJobResult`.

Properties
NameTypeRequiredDescription
diagnosticsCrawlDiagnosticsno
pageCountintegeryes
urlsarray<string>yes

Used by: POST /sites/{siteId}/crawl, POST /sites/{siteId}/recrawl

CurlResponse

Properties
NameTypeRequiredDescription
bodystringyesResponse body content
errornull | FetchErrorno
headersmap<string, string>yesLower-cased response headers. Repeated values are comma-joined.
statusCodeinteger (int32)yesHTTP status code from the fetched URL
urlstringyesThe URL that was fetched

Used by: GET /curl.json

CwvAssessment

Whether the page or origin passes the Core Web Vitals assessment.

Type: string one of "passed", "failed", "insufficientData", "unknown"

DelegatedAsset

An asset returned to an operational Dispatcher. The root `ownerId` bearer is intentionally absent; operational configuration stays available.

Properties
NameTypeRequiredDescription
bookingFieldsarray<BookingFieldDef>no
createdAtinteger (int64)yes
currencystringyes
descriptionstring | nullno
idstringyes
kindstringyes
namestringyes
policynull | BookingPolicyno
promoCodesarray<PromoCode>no
timezonestringyes

DelegatedAssetWithWindows

Properties
NameTypeRequiredDescription
assetDelegatedAssetyes
windowCountinteger (int32)yes
windowsarray<Window>yes

DelegatedAuditEvent

A delegated audit row. Reservation/waitlist IDs are public mutation capabilities and detail is free-form, potentially credential-bearing text, so neither crosses this non-root boundary. Dispatchers get their booking capabilities from the operational asset/window/reservation surfaces.

Properties
NameTypeRequiredDescription
actionstringyes
assetIdstring | nullno
atinteger (int64)yes
windowIdstring | nullno

DeliveryOutcome

Type: string one of "pending", "accepted", "queued", "failed", "cancelled"

DiagnosticSample

One concrete failing-URL example carried in [`CrawlDiagnostics::samples`]. A bounded list of examples so an operator sees *which* URLs failed and *why*, not just a count. `detail` carries the HTTP status text or content-type when the reason has one.

Properties
NameTypeRequiredDescription
detailstring | nullno
reasonstringyesReason tag, matching a `CrawlDiagnostics` counter: one of `httpError`, `nonHtml`, `fetchError`, `robotsBlocked`, `scopeFiltered`, `depthFiltered`, `indexError`. (Frontier sheds carry no samples — they are counted in bulk, never per URL.)
urlstringyes

DiscountKind

How a [`PromoCode`] reduces a window's per-unit cost.

Type: string one of "percent", "fixed"

DiscoverySource

How a URL entered the bounded crawl sample.

Type: string one of "target", "sitemap", "homepage-link", "site-search"

Echo

JSON payload for the echo endpoint. Mirrors the schema of `fsly::echo::Echo`, plus api-specific request-derived bot/fraud signals.

Properties
NameTypeRequiredDescription
datastringyesRequest body data (empty for GET requests)
fingerprintstringyesSHA-256 fingerprint derived from selected headers
fraudnumber (float)yesFraud score (0.0 = trusted, 1.0 = highly suspicious). Combines cheap header/UA/JA3-JA4 heuristics with the IP/TLS reputation feeds (datacenter, VPN, Spamhaus DROP, abuse.ch SSLBL JA3).
headersmap<string, array<string>>yesRequest headers (sorted alphabetically)
hoststringyesHost header value
iCloudPrivateRelaybooleanyesTrue if IP is in iCloud Private Relay range
ipstringyesClient IP address
ja3stringyesJA3 TLS fingerprint hash (if available from upstream proxy)
methodstringyesHTTP method (GET, POST, PUT, DELETE, etc.)
querymap<string, array<string>>yesParsed query parameters
schemestringyesURL scheme (http or https)
sessionstringyesSession hash (fingerprint + IP + cookie)
torExitNodebooleanyesTrue if IP is a known Tor exit node
uristringyesRequest URI path
versionstringyesHTTP version

Used by: GET /echo.json

EmailDelivery

Properties
NameTypeRequiredDescription
alertAtinteger (int64)yes
idstringyes
kindstringyes
stateDeliveryOutcomeyes
updatedAtMsinteger (int64)yes

EmailNotificationStatus

Properties
NameTypeRequiredDescription
deliverynull | EmailDeliveryno
emailstring | nullno
pendingbooleanyes
pendingEmailstring | nullno
pendingExpiresAtMsinteger | null (int64)no
verifiedbooleanyes

Used by: GET /audit/watches/{id}/notifications/email, POST /audit/watches/{id}/notifications/email/verify

EncodingResponse

Response payload for `/encoding.json`. Field names are camelCase to match the `/...json` API conventions.

Properties
NameTypeRequiredDescription
base64DecodedstringyesBase64-decoded input as text. Empty when the input is not valid Base64 **or** when it decodes to bytes that are not text in the applied charset — read `base64Valid` and `base64DecodedBytes` to tell those two cases apart.
base64DecodedBytesintegeryesNumber of bytes the input decoded to; `0` when `base64Valid` is false. A non-zero count with an empty `base64Decoded` means the input is valid Base64 carrying binary data rather than text.
base64EncodedstringyesBase64-encoded representation of raw bytes, standard alphabet (RFC 4648 section 4), padded with `=`.
base64UrlEncodedstringyesBase64-encoded representation of raw bytes, URL- and filename-safe alphabet (RFC 4648 section 5: `-` and `_` for `+` and `/`) with padding omitted — the form JWT/JOSE (RFC 7515) mandates.
base64ValidbooleanyesWhether the input value parses as Base64 at all. The decoder accepts both the standard and the URL-safe alphabet and tolerates missing padding, so this is `true` for far more inputs than it looks — any four-character run of alphabet characters decodes to three bytes.
binarystringyesRaw bytes in base 2, eight digits per byte, space separated. Unsigned, unlike the signed `octal` / `decimal` / `hex` views above, because a negative binary byte is not a thing anyone means by "text to binary".
byteLengthintegeryesByte count of the raw input under the applied charset. Differs from `rawLength` for any non-ASCII input (`ä` is one character, two bytes).
charsetstringyesApplied charset (may differ from requested if unsupported)
decimalstringyesDecimal representation of raw bytes
hashinteger (int64)yesFNV-1a 64-bit hash of the raw input. A non-cryptographic checksum for bucketing and change detection — deliberately named in the docs so callers can reproduce it, and deliberately not a substitute for the SHA-256 above.
hexstringyesHexadecimal representation of raw bytes
hexDecodedstringyesThe input read *as* hex and decoded back to text. Tolerates whitespace, a leading `0x`, and `:` or `-` separators. Empty when the input is not hex or does not decode to text — read `hexValid` and `hexDecodedBytes` to tell those apart, exactly as with Base64.
hexDecodedBytesintegeryesNumber of bytes the input decoded to when read as hex; `0` when `hexValid` is false.
hexEncodedstringyesRaw bytes as one contiguous lowercase hex string. This is the form `xxd -p`, checksums and wire dumps use — distinct from the bracketed, signed, Kotlin-style `hex` field above.
hexValidbooleanyesWhether the input parses as hex. Requires an even number of hex digits after separators are removed.
htmlDecodedstringyesHTML character references in the input resolved back to text. Covers every numeric reference (`&#169;`, `&#xA9;`) plus the HTML 4.01 Latin-1, special and common symbol named entities. An unrecognised entity is left exactly as it stands rather than dropped, so decoding never silently loses text.
htmlEncodedstringyesHTML-escaped input: `&`, `<`, `>`, `"` and `'` become character references, so the value is safe to interpolate into element text or a quoted attribute. `'` uses the numeric `&#39;` rather than `&apos;`, which HTML 4 does not define.
jsonEscapedstringyesThe input as a complete, quoted JSON string literal, ASCII-safe: every non-ASCII character is written as `\uXXXX` (surrogate pairs above U+FFFF). Paste-ready into JSON, JavaScript, Java or any other format that takes JSON string syntax — the surrounding quotes are included.
jsonUnescapedstringyesThe input read *as* a JSON string literal, with `\n`, `\t`, `\uXXXX` and friends resolved. The surrounding quotes are optional. Empty when the input is not a well-formed JSON string; see `jsonValid`.
jsonValidbooleanyesWhether the input parses as a JSON string literal.
md5stringyesMD5 hash as lowercase hex
octalstringyesOctal representation of raw bytes
rawstringyesOriginal input value
rawLengthintegeryesCharacter count of raw input (Unicode scalar values, not UTF-16 units)
sha1stringyesSHA-1 hash as lowercase hex
sha256stringyesSHA-256 hash as lowercase hex
urlDecodedstringyesURL/percent-decoded input
urlEncodedstringyesURL/percent-encoded representation

Used by: GET /encoding.json

EngagementAggregate

Aggregate engagement depth over a window of visits — returned as `VisitStats.engagement` and rendered on the dashboard as a KPI row. Answers *"how sticky was this site?"* with three ROBUST numbers (medians rather than means, so a couple of tab-open-for-hours outliers don't skew) plus one marketing KPI (`engaged_rate_pct`).

Properties
NameTypeRequiredDescription
engagedRatePctnumber | null (double)noMarketing KPI — share of samples that count as "engaged": time on page ≥ 10 s, OR scroll depth ≥ 50 %, OR at least one interaction. In percent (0..=100).
p50Interactionsinteger | null (int32)noMedian interaction count per visit (0..3 for the three counters: click, keydown, pointerdown).
p50ScrollPctnumber | null (double)noMedian deepest scroll depth reached, as a percentage of document height (0..=100).
p50TimeOnPageMsinteger | null (int64)noMedian visible time on page (ms) across all samples in the window. Client-side already capped at 30 min, so no outlier truncation here.
p75TimeOnPageMsinteger | null (int64)noP75 visible time on page (ms) — surfaces the tail (how long the stickiest quartile actually stayed).
samplesinteger (int64)yesTotal number of visits in the window that contributed at least one engagement field. When zero, all fields are `None`.

EntropyResponse

Mirrors the Kotlin `Entropy` DTO shape, but served as `/entropy.json`. Kotlin source (for reference): - uuid: UUID.randomUUID() - secureRandomLong: Random().nextLong() - secureRandomFloat: Random().nextFloat() - secureRandomGaussian: Random().nextGaussian() - secureRandomInt: Random().nextInt() - timestamp: Instant.now() Rust note: - We intentionally do *not* use "secure" entropy sources; we stick to a fast RNG suitable for non-cryptographic "jitter" / uniqueness. - Timestamp is ISO 8601 (RFC 3339) in UTC, which is the natural JSON representation in Rust. (Kotlin/Java `Instant` also serializes as an ISO timestamp under typical Jackson configs.)

Properties
NameTypeRequiredDescription
secure_random_floatnumber (float)yesRandom float in range [0.0, 1.0)
secure_random_gaussiannumber (double)yesRandom Gaussian-distributed double (mean=0, stddev=1)
secure_random_intinteger (int32)yesRandom 32-bit signed integer
secure_random_longinteger (int64)yesRandom 64-bit signed integer
timestampstringyesCurrent timestamp as epoch milliseconds
uuidstringyesRandom UUID v4

Used by: GET /entropy.json

FetchError

Structured MCP fetch failure. HTTP responses, including 4xx/5xx, are not failures and remain [`CurlResponse`] values. This type is reserved for validation, SSRF, transport, and bounded-body failures.

Properties
NameTypeRequiredDescription
codestringyes
kindFetchErrorKindyes
messagestringyes
retryAfterstring | nullno
statusinteger | null (int32)no

FetchErrorKind

JSON response payload (mirrors Kotlin `Curl`).

Type: string one of "invalid_request", "ssrf", "dns", "tls", "timeout", "rate_limited", "body_cap", "upstream", "transport"

FetchedPage

Response DTO for fetched/indexed pages. Matches the Kotlin `FetchedPage` structure.

Properties
NameTypeRequiredDescription
bodystring | nullno
idstring | nullno
labelsarray<string>no
siteIdstring | null (uuid)no
thumbnailstring | nullno
titlestring | nullno
updatedstring | nullno
urlstring | nullno

Used by: GET /pages/{id}, GET /sites/{siteId}/pages, PUT /sites/{siteId}/pages, PUT /sites/{siteId}/pages/{pageId}

FieldData

Where real-user field data comes from and what it says. The only source is Google's Chrome UX Report: public, aggregated 75th-percentile data from real Chrome visits. Without a configured API key the state is `notConfigured` and nothing else is set.

Properties
NameTypeRequiredDescription
collectionPeriodnull | CollectionPeriodno
coreWebVitalsnull | CwvAssessmentno
formFactorstringno`all`: every device type combined.
keystring | nullnoThe URL or origin the record is for.
metricsarray<FieldMetric>no
scopenull | FieldDataScopeno
sourcestringno`chromeUxReport`.
stateFieldDataStateno
unavailableReasonstring | nullnoWhy the data is `unavailable`, for example `timeout`.

FieldDataScope

Type: string one of "url", "origin", "unknown"

FieldDataState

Type: string one of "notConfigured", "insufficientData", "complete", "unavailable"

FieldMetric

One real-user metric at the 75th percentile. The engine always sets `p75` and both thresholds; they are optional so that a stored report stays readable when a number is missing or `null`, which is how JSON carries a non-finite one.

Properties
NameTypeRequiredDescription
coreWebVitalbooleannoLCP, INP, and CLS are Core Web Vitals; FCP and TTFB are diagnostics.
cruxMetricstringnoThe Chrome UX Report metric name.
distributionnull | RatingDistributionno
goodThresholdnumber | null (double)no
idstringno`lcp`, `inp`, `cls`, `fcp`, or `ttfb`.
p75number | null (double)no
poorThresholdnumber | null (double)no
ratingMetricRatingno
unitstringno`millisecond` or `unitless`.

FieldType

The value shape of an owner-defined [`BookingFieldDef`]. `Select` constrains the value to one of the field's `options`; the rest are free-form text / numeric / boolean values validated on booking.

Type: string one of "text", "number", "bool", "select"

FindingDetail

A finding plus everything a reader needs to act on it.

Properties
NameTypeRequiredDescription
categorystringnoReport section this belongs to.
docsstring | nullnoPrimary specification or reference, when one exists.
howstringnoHow to address it.
idstringnoStable catalogue id, e.g. `header.csp.missing`. Safe to match on.
messagestringnoThe same sentence that appears in `findings`.
severitystringnoAdvisory triage — independent of the frozen 0–100 score.
titlestringnoShort label for the finding kind.
whatstringnoWhat the thing being reported actually is.
whystringnoWhy it matters — the concrete consequence, not a platitude.

FoundPage

Found page in search results. Equivalent to Kotlin `FoundPage`.

Properties
NameTypeRequiredDescription
bodystring | nullno
labelsarray<string>no
thumbnailstring | nullno
titlestring | nullno
urlRawstring | nullno

HarExportQuery

Query parameters for the HAR export endpoint. All parameters are optional. When none are provided, returns all HAR entries (up to the default limit).

Properties
NameTypeRequiredDescription
beforestring | nullnoISO 8601 datetime: only include entries crawled before this time
contentTypestring | nullnoFilter by MIME type (e.g. "text/html")
limitinteger | nullnoMaximum entries to return (default: 100, max: 10000)
maxTimenumber | null (double)noMaximum response time in milliseconds
minTimenumber | null (double)noMinimum response time in milliseconds
offsetinteger | nullnoNumber of entries to skip (default: 0)
sincestring | nullnoISO 8601 datetime: only include entries crawled at or after this time
sortstring | nullnoSort field: "time", "status", "crawledAt" (default); prefix with "-" for descending
statusinteger | null (int32)noFilter by HTTP status code (e.g. 200, 404)
urlstring | nullnoURL prefix match (e.g. "https://example.com/blog")

HealthCheck

One stable weighted health check. `weight` is the model's nominal weight; only pass/fail checks contribute to `evaluatedWeight`.

Properties
NameTypeRequiredDescription
idstringno
reasonstringno
statusHealthCheckStatusno
weightinteger (int32)no

HealthCheckStatus

Type: string one of "pass", "fail", "notApplicable", "unknown"

HealthEvaluationState

Type: string one of "complete", "partial", "notEvaluated"

HealthReport

Versioned successor to the frozen top-level health score.

Properties
NameTypeRequiredDescription
checksarray<HealthCheck>no
evaluatedWeightinteger (int32)no
evaluationReasonsarray<string>no
evaluationStateHealthEvaluationStateno
scoreinteger | null (int32)no
scoreModelVersioninteger (int32)no

HtmlSpeedMetrics

Speed signals counted in the page's source HTML.

Properties
NameTypeRequiredDescription
crossOriginScriptHostsarray<string>noHosts other than the page's that serve its external scripts, sorted, at most 20.
dnsPrefetchHintsintegerno
domElementsintegernoElements inside `<body>`.
firstImageLazybooleannoThe first `<img>` in source order has `loading=lazy`.
headStylesheetsintegernoStylesheets in `<head>` for all media or screen. Browsers wait for these before the first render, so they are counted, not flagged.
imagesintegerno`<img>` elements.
imagesWithoutDimensionsintegernoImages lacking a `width` or a `height` attribute.
lazyImagesintegernoImages with `loading=lazy`.
preconnectHintsintegerno
preloadHintsintegerno
preloadWithoutAsintegernoPreload hints without an `as` attribute.
renderBlockingScriptsintegernoExternal classic scripts in `<head>` without `async` or `defer`.

ImportResult

Counts of what an `import` wrote.

Properties
NameTypeRequiredDescription
assetsinteger (int32)yes
membersinteger (int32)yes
reservationsinteger (int32)yes
webhooksinteger (int32)yes
windowsinteger (int32)yes

Used by: POST /assets/owners/{ownerId}/import

Inspection

Response payload for `/inspect.json`.

Properties
NameTypeRequiredDescription
echoEchoyes
whoisWhoisyes

Used by: GET /inspect.json

LabTiming

Wall-clock timing of the page request, measured from after the audit's concurrency permits were granted, so queueing behind other probes is not included. Connection setup, TLS, and any followed redirects are.

Properties
NameTypeRequiredDescription
bodyCompletebooleannoFalse when the body was truncated at the audit's cap or interrupted, so `totalMs` covers only the captured prefix.
responseHeadersMsinteger (int32)noMilliseconds until the final response's headers arrived.
totalMsinteger (int32)noMilliseconds until the (capped) body was read.

Member

A role-scoped delegated credential for an owner's team. `id` is the member's bearer token — it is used *in place of the ownerId* in the manager URL and is also the revoke handle. Additive to the single-bearer model: the root ownerId keeps working and is the only credential that can mint/revoke members.

Properties
NameTypeRequiredDescription
createdAtinteger (int64)yes
idstringyes
labelstringyes
roleRoleyes

Used by: GET /assets/owners/{ownerId}/members, POST /assets/owners/{ownerId}/members

MetricRating

Type: string one of "good", "needsImprovement", "poor", "unrated"

ModifyReservation

JSON body to modify an existing active booking in place — change the seat `quantity`, replace the custom `fields`, and/or edit the `note`. Every field is optional (omit to leave unchanged; a `note` of `""` clears it). A quantity increase is capacity-checked (may 409 `slot_full`); a decrease frees seats and may auto-promote a waitlister. Cost/currency/promo stay as booked.

Properties
NameTypeRequiredDescription
fieldsmap<string, string>no
notestring | nullno
quantityinteger | null (int32)no

Used by: PATCH /assets/owners/{ownerId}/reservations/{reservationId}/modify, PATCH /assets/reservations/{reservationId}/modify

NewAsset

JSON body to create an asset (the server mints `id`/`ownerId`/`createdAt`).

Properties
NameTypeRequiredDescription
bookingFieldsarray<BookingFieldDef>noOwner-defined fields a consumer fills at booking (empty = none).
currencystringno
descriptionstring | nullno
kindstringno
namestringyes
policynull | BookingPolicyno
promoCodesarray<PromoCode>noOwner-defined discount codes (empty = none).
timezonestringnoIANA timezone the slots are read in (default `UTC`).

Used by: POST /assets/owners/{ownerId}/assets

NewAvailability

JSON body to bulk-generate a recurring grid of windows. Timezone-free: all times are absolute epoch-ms. Example — eight 1-hour slots each day for a week: `{firstStart, slotMinutes:60, slots:8, repeatEveryMinutes:1440, repeatCount:7, cost, capacity}`.

Properties
NameTypeRequiredDescription
capacityinteger (int32)no
costnumber (double)yes
firstStartinteger (int64)yesEpoch-ms start of the first slot.
repeatCountinteger (int32)noNumber of repetitions (0/1 = the block once).
repeatEveryMinutesinteger (int32)noOffset between repetitions, minutes (0 = no repetition — a single block).
slotMinutesinteger (int32)yesLength of each slot, minutes.
slotsinteger (int32)yesNumber of back-to-back slots per repetition.

Used by: POST /assets/owners/{ownerId}/assets/{assetId}/availability

NewHold

JSON body to place a **tentative hold** on a seat. Like [`NewReservation`] but the booking auto-releases after `ttlSeconds` unless promoted with `confirm`.

Properties
NameTypeRequiredDescription
consumerstringyes
fieldsmap<string, string>noValues for the asset's custom booking fields, keyed by field `key`.
notestring | nullno
promoCodestring | nullnoOptional discount code (case-insensitive).
quantityinteger (int32)noUnits/seats to hold in this one call (default 1).
ttlSecondsinteger (int64)yesSeconds the hold lives before it auto-releases the seat (`HOLD_TTL_MIN_SECS`..=`HOLD_TTL_MAX_SECS`).

Used by: POST /assets/asset/{assetId}/windows/{windowId}/hold

NewMember

JSON body to mint a member token (server mints `id`/`createdAt`).

Properties
NameTypeRequiredDescription
labelstringnoOptional human label (defaults to the role name).
rolestringyes`dispatcher` (read + book + schedule) or `viewer` (read-only).

Used by: POST /assets/owners/{ownerId}/members

NewReservation

JSON body a consumer POSTs to book a window.

Properties
NameTypeRequiredDescription
consumerstringyes
fieldsmap<string, string>noValues for the asset's custom booking fields, keyed by field `key`.
notestring | nullno
promoCodestring | nullnoOptional discount code (case-insensitive); applies the asset's matching [`PromoCode`] to the snapshotted per-unit cost.
quantityinteger (int32)noUnits/seats to reserve in this one call (default 1; must fit the window's remaining capacity and not exceed its total capacity).

Used by: POST /assets/asset/{assetId}/windows/{windowId}/reserve

NewWaitlist

JSON body to join a window's waitlist.

Properties
NameTypeRequiredDescription
consumerstringyes
fieldsmap<string, string>noCustom booking-field values to carry onto the promoted hold.
notestring | nullno
quantityinteger (int32)noSeats wanted (default 1; must not exceed the window's total capacity).

Used by: POST /assets/asset/{assetId}/windows/{windowId}/waitlist

NewWebhook

JSON body to register a webhook (server mints `id`/`createdAt`, sanitizes `url`).

Properties
NameTypeRequiredDescription
eventsarray<string>noEmpty ⇒ subscribe to everything (`["*"]`).
urlstringyes

Used by: POST /assets/owners/{ownerId}/webhooks

NewWindow

JSON body to add a single utilization window (server mints `id`).

Properties
NameTypeRequiredDescription
capacityinteger (int32)noHow many can book this slot; defaults to 1.
costnumber (double)yes
endinteger (int64)yes
startinteger (int64)yes

Used by: POST /assets/owners/{ownerId}/assets/{assetId}/windows

OutboundAggregate

Aggregate outbound clicks & downloads over a window of visits — returned as `VisitStats.outbound` and rendered on the dashboard as two side-by-side TOP-N panels. Answers *"which off-site destinations do my visitors care about, and what are they downloading?"* with counted-frequency tables. Rows are per-*event* (not per-visit): a single visit that clicked three outbound links contributes three to `total_outbound_clicks` and can appear three times in `top_hosts`.

Properties
NameTypeRequiredDescription
samplesinteger (int64)yesTotal number of visits in the window that opted into `?outbound=true` (whether or not they actually clicked anything).
topDownloadsarray<OutboundCountRow>yesTop download file extensions by event count (descending), capped at [`TOP_N`]. Empty when no outbound event carried a download extension.
topHostsarray<OutboundCountRow>yesTop external destination hosts by event count (descending), capped at [`TOP_N`]. Empty when no outbound event carried a host.
totalDownloadsinteger (int64)yesTotal download events (i.e. events with a `downloadExt` set) in the window. Same double-counting note as `total_outbound_clicks`.
totalOutboundClicksinteger (int64)yesTotal outbound-click events (i.e. events with a `host` set) in the window. A download from an external host contributes to both this total *and* `total_downloads`.

OutboundCountRow

One row of a TOP-N outbound breakdown: a label (external destination host, or a lowercased download file extension) with the count of matching events in the window. Simpler than [`CountRow`] because the outbound aggregation is per-event (not per-visit), so unique-visitor and human/bot splits don't apply — an outbound click is either recorded or not.

Properties
NameTypeRequiredDescription
countinteger (int64)yesCount of matching events in the window.
labelstringyesExternal destination host (e.g. `github.com`) or download file extension (e.g. `pdf`), lowercased.

OutboundEvent

One outbound-click sample: an anchor click that pointed to another host (`host` set), or downloaded a known file type (`download_ext` set), or both. Emitted zero-or-more times per visit; batched into [`PageOutbound::events`] and beaconed at visit end. Never contains the raw href — only the destination host (used for TOP-N-hosts breakdowns) and/or the lowercased file extension (used for TOP-N-downloads breakdowns). URL paths, query strings, and fragments are dropped in the browser before the beacon.

Properties
NameTypeRequiredDescription
downloadExtstring | nullnoThe download file extension (lowercased, no dot; e.g. `pdf`) when the href path ends with a known download extension; `None` for a bare external-navigation click.
hoststring | nullnoThe destination host (e.g. `github.com`) when the click leaves the current site; `None` for same-host downloads. Lowercased.

OwnerExport

A complete, portable snapshot of an owner's data — assets (with their windows and bookings), members, and webhooks. The operator owns this: back it up, or re-`import` it into a fresh owner on another self-hosted instance (ids are preserved, so share links and booking receipts keep working). Audit log and waitlist queues (transient) are intentionally excluded.

Properties
NameTypeRequiredDescription
assetsarray<AssetWithWindows>no
exportedAtinteger (int64)yes
membersarray<Member>no
ownerIdstringyes
versioninteger (int32)yes
webhooksarray<Webhook>no

Used by: GET /assets/owners/{ownerId}/export, POST /assets/owners/{ownerId}/import

OwnerReport

An owner utilization + revenue report — pure aggregation over the owner's assets/windows/reservations (no new storage). Snapshot at `generatedAt`.

Properties
NameTypeRequiredDescription
assetsinteger (int32)yesNumber of assets the owner publishes.
bookingsBookingCountsyes
generatedAtinteger (int64)yesEpoch-ms the report was computed.
pastinteger (int32)yesActive bookings whose window has already ended.
revenueByCurrencymap<string, number (double)>yesRealized/committed revenue (confirmed + completed) per currency; tentative holds are excluded.
seatsBookedinteger (int64)yesSeats consumed by active (non-cancelled, non-expired) bookings.
seatsTotalinteger (int64)yesSum of window capacities (total bookable seats).
upcominginteger (int32)yesActive bookings whose window starts in the future.
utilizationPctnumber (double)yes`seatsBooked / seatsTotal * 100`, or `0` when there are no seats.
windowsinteger (int32)yesNumber of utilization windows across all assets.

Used by: GET /assets/owners/{ownerId}/report

PageEncodingProvenance

Character-decoding evidence for a fetched HTML page. Raw byte size and hash remain byte-based; this only describes the decoded view used by analyzers.

Properties
NameTypeRequiredDescription
encodingstringnoCanonical WHATWG encoding name, for example `UTF-8` or `windows-1251`.
hadErrorsbooleannoWhether malformed byte sequences required replacement characters.
sourcestringno`bom`, `httpHeader`, `htmlMeta`, `xmlDeclaration`, or `fallback`.

PageEngagement

Engagement-depth sample for one visit, opt-in via `/analytics.js?engagement=true`. A cheap "how sticky was this pageview" complement to Core Web Vitals — the three fields answer *were they still there, how far did they read, did they touch anything?* No DOM contents, no URLs, no ids; only structural counters that describe the visit's shape. All fields optional so partial reports (tab closed pre-interaction, JS scroll disabled, etc.) still record.

Properties
NameTypeRequiredDescription
interactionsinteger | null (int32)noCount of primary interactions during the visit: `click`, `keydown` (excluding modifier-only), and `pointerdown`. Deduped per-event-type so keyboard-repeat doesn't inflate the number.
scrollPctnumber | null (double)noDeepest scroll reached during the visit, as a percentage of document height (0..=100). `scrollY + innerHeight` over `scrollHeight`, ratcheted upward — never decreases on scroll-back.
timeOnPageMsinteger | null (int64)noWall-clock time the tab was foregrounded, ms — sum of visible spans between `visibilitychange`s. Capped at 30 min client-side to bound the aggregation tail (a tab left open overnight is not engagement).

Used by: POST /visit/engagement.json

PageMeta

SEO / correctness facts extracted from the page's HTML `<head>` + body. Presence-and-shape only — no scoring here; the report folds the notable gaps into `findings`.

Properties
NameTypeRequiredDescription
canonicalstring | nullno`<link rel=canonical>` href.
charsetDeclaredbooleannoThe document declares its character encoding in the markup: a `<meta charset>` or a `<meta http-equiv=content-type>` whose content carries `charset=`. Pages relying on the Content-Type header alone are still fine — [`quality_findings`] only flags the case where neither declares one.
descriptionstring | nullno`<meta name=description>` content.
h1CountintegernoCount of `<h1>` elements (0 or >1 are both SEO smells).
jsonLdbooleannoAt least one `<script type=application/ld+json>` structured-data block.
langstring | nullno`<html lang>` value.
mixedContentarray<string>no`http://` sub-resource URLs referenced from an `https://` page — active mixed content the browser will block or downgrade the padlock over. Capped to a handful for report compactness.
ogImagebooleannoOpen Graph image (`og:image`) present.
ogTitlebooleannoOpen Graph title (`og:title`) present.
robotsstring | nullno`<meta name=robots>` content (e.g. `noindex,nofollow`).
titlestring | nullno
titleAssertablebooleannoAn exported Hurl suite may assert `title`: hurl, which decodes the whole body with the charset the Content-Type declares (UTF-8 when it declares none) and parses it with libxml2, and zforce, which reads the body as UTF-8, both read exactly this text through `normalize-space(//head/title)`. False when either would read another text - a no-break or ideographic space, a character reference they decode differently, a second or commented-out `<title>`, a body in another charset - and in reports captured before it was recorded.
viewportbooleannoA responsive `<meta name=viewport>` is present.

PageOutbound

Outbound-click / download batch for one visit, opt-in via `/analytics.js?outbound=true`. Beaconed once at `visibilitychange -> hidden` / `pagehide` (same pattern as vitals and engagement) so a single flush captures the whole visit. `events` may be empty — the collector still beacons at least once so the aggregation counts the visit as "opted in but had zero outbound events", distinguishing it from a site that never opted in at all.

Properties
NameTypeRequiredDescription
eventsarray<OutboundEvent>noZero-or-more outbound events accumulated during the visit.

Used by: POST /visit/outbound.json

PageVitals

Core Web Vitals sample for one visit, opt-in via `/analytics.js?vitals=true`. The four fields are Google's field-metric quartet (LCP + INP + CLS + TTFB); all are optional because a visit may report a partial set — e.g. the tab is closed before INP fires, or the page has zero interactions. Absent values are simply dropped from the aggregate; no imputation. Numeric ranges follow Google's own definitions: LCP / INP / TTFB in milliseconds; CLS unitless. Only structural / performance data; **no** DOM contents, no URLs, no ids.

Properties
NameTypeRequiredDescription
clsnumber | null (double)noCumulative Layout Shift (unitless, typically 0..1).
inpMsnumber | null (double)noInteraction to Next Paint (ms) — worst input-response latency in the session. Absent on visits with zero real interactions.
lcpMsnumber | null (double)noLargest Contentful Paint (ms) — main-content load time.
ttfbMsnumber | null (double)noTime to First Byte (ms) — network + server first-byte time from `performance.getEntriesByType('navigation')[0].responseStart`.

Used by: POST /visit/vitals.json

PerformanceReport

Advisory speed section of a one-page report. Never scored, never diffed.

Properties
NameTypeRequiredDescription
checksarray<SeoCheck>noAdvisory checks, category `performance`. None is weighted, `Fail`, or `High`, and none reaches `seo.checks`, the SEO score, or JUnit.
fieldDataFieldDatano
htmlnull | HtmlSpeedMetricsno
labTimingnull | LabTimingno
modelVersioninteger (int32)no[`PERFORMANCE_MODEL_VERSION`] of the rules that produced this section.
transportTransportEvidenceno

ProbeResult

One probed URL's result.

Properties
NameTypeRequiredDescription
classstringnoClassification: `present` / `missing` (404) / `redirected` (3xx) / `soft-404` (200 + HTML where a non-HTML well-known file is expected) / `stale` (a 2xx security.txt past its RFC 9116 `Expires` timestamp) / `error` (4xx–5xx) / `unreachable` (no response).
contentTypestring | nullno
errorstring | nullnoSet instead of a status when the probe could not complete.
errorKindstring | nullnoStable, additive classification for a transport failure. When no response arrived, the legacy `error: "fetch_error"` value remains unchanged for compatibility. A failure after response headers instead retains the status, sets `truncated`, and records its kind here.
finalUrlstring | nullnoThe URL the probe actually landed on, set **only** when the client followed one or more redirects and the final URL differs from the requested one. Surfaces the redirect chain's endpoint (e.g. an `http→https` upgrade or a `www` canonicalisation) that a status-only view hides once the hops are auto-followed.
headersmap<string, string>noCurated response headers (lower-cased keys; see [`REPORTED_HEADERS`]).
okbooleannoTrue when a response arrived with a 2xx/3xx status.
pathstringnoLogical label: `(page)` for the target, else the well-known path.
sha256string | nullno
sizeinteger | nullno
statusinteger | null (int32)no
truncatedbooleannoTrue if the body hit [`MAX_PROBE_BYTES`] or its stream was interrupted; in either case the reported hash and size cover only the captured prefix.
urlstringnoThe absolute URL probed.

PromoCode

An owner-defined discount code a consumer supplies at booking. Unlimited-use within an optional validity window; the discounted per-unit price is snapshotted onto the [`Reservation`] (`cost`) and the code recorded (`appliedCode`). (Per-code usage caps are a deferred additive extension.)

Properties
NameTypeRequiredDescription
amountnumber (double)yesPercentage (`0..=100`) for `percent`, or a flat amount for `fixed`.
codestringyesCase-insensitive code the consumer types (stored upper-cased).
kindDiscountKindno
validFrominteger | null (int64)noOptional epoch-ms lower bound the code becomes valid (inclusive).
validUntilinteger | null (int64)noOptional epoch-ms upper bound the code stays valid (exclusive).

PublicWindow

A window as seen by a *consumer*: availability only — never a booker's details.

Properties
NameTypeRequiredDescription
capacityinteger (int32)yes
costnumber (double)yes
endinteger (int64)yes
idstringyes
remaininginteger (int32)yes
reservedbooleanyesConvenience flag: `remaining == 0`.
startinteger (int64)yes
waitlistedinteger (int32)yesHow many consumers are queued on this window's waitlist.

RatingDistribution

Share of page loads in each rating band, 0.0 to 1.0. Each share is optional so that a stored report stays readable when one is missing or `null`.

Properties
NameTypeRequiredDescription
goodnumber | null (double)no
needsImprovementnumber | null (double)no
poornumber | null (double)no

RescheduleRequest

JSON body to reschedule a booking onto a different window of the same asset.

Properties
NameTypeRequiredDescription
windowIdstringyes

Used by: POST /assets/reservations/{reservationId}/reschedule

Reservation

A consumer's booking of a window. `id` doubles as the booker's capability to cancel/reschedule it. Snapshots cost + currency at booking time.

Properties
NameTypeRequiredDescription
appliedCodestring | nullnoThe promo code applied to this booking, if any (upper-cased).
assetIdstringyes
consumerstringyesWho booked — a free-form name or email the consumer supplies.
costnumber (double)yesPer-unit price snapshotted at booking time — **already discounted** if an `appliedCode` promo was used. Booking total = `cost * quantity`.
currencystringyes
fieldsmap<string, string>noConsumer-supplied values for the asset's [`BookingFieldDef`]s (empty = none).
holdExpiresAtinteger | null (int64)noWhen a `Held` booking auto-releases (epoch-ms UTC). Set only while the reservation is a live hold; cleared once confirmed.
idstringyesBooking receipt id (UUID) — also the consumer's cancel/reschedule capability.
notestring | nullno
quantityinteger (int32)noHow many capacity units this booking consumes (group/quantity booking). Defaults to 1; `cost` is the per-unit window cost (total = `cost * quantity`).
reservedAtinteger (int64)yes
statusReservationStatusno
windowIdstringyes

Used by: POST /assets/asset/{assetId}/windows/{windowId}/hold, POST /assets/asset/{assetId}/windows/{windowId}/reserve, POST /assets/owners/{ownerId}/reservations/{reservationId}/cancel, PATCH /assets/owners/{ownerId}/reservations/{reservationId}/modify, POST /assets/reservations/{reservationId}/cancel, POST /assets/reservations/{reservationId}/confirm, PATCH /assets/reservations/{reservationId}/modify, POST /assets/reservations/{reservationId}/reschedule

ReservationRow

One row of the manager's booking list — a reservation with asset/window context.

Properties
NameTypeRequiredDescription
assetIdstringyes
assetNamestringyes
endinteger (int64)yes
reservationReservationyes
startinteger (int64)yes
windowIdstringyes

ReservationRowsResponse

Role-dependent serialization for the manager reservation list.

Type: array<ReservationRow> | array<ViewerReservationRow>

Used by: GET /assets/owners/{ownerId}/reservations

ReservationStatus

The lifecycle state of a booking. `Confirmed`, `Completed` and (an unexpired) `Held` consume capacity; `Cancelled` and `Expired` free it. `Held` is a *tentative* reservation minted by [`AssetStore::hold`] that auto-releases at its `holdExpiresAt` — promoted to `Confirmed` by [`AssetStore::confirm`], or swept to `Expired` when its deadline passes. (Delivery-specific states — dispatched/delivered/failed — are a planned additive extension of this enum.)

Type: string one of "confirmed", "cancelled", "completed", "held", "expired"

Role

The effective authority a manager-surface caller holds. The root `ownerId` resolves to [`Role::Owner`] (full control); a minted member token resolves to its scoped role. Only `Dispatcher`/`Viewer` are mintable as members.

Type: string one of "owner", "dispatcher", "viewer"

SearchResult

Search result DTO. Equivalent to Kotlin `Result`.

Properties
NameTypeRequiredDescription
querystringyes
resultsarray<FoundPage>yes

Used by: GET /sites/{siteId}/search

SecurityAnalysis

Beyond mere presence: the *quality* of a page's security headers. A header can be present yet toothless (a `Content-Security-Policy` allowing `unsafe-inline`, an `HSTS` with a one-second `max-age`), which a presence-only check scores identically to a strong one. This captures the distinctions that matter and rolls them into a letter [`grade`].

Properties
NameTypeRequiredDescription
cspbooleanno`Content-Security-Policy` present at all.
cspStyleUnsafeInlinebooleannoThe effective STYLE policy (`style-src`, else `default-src`) permits `unsafe-inline` — reported separately from the script-level loosening because inline styles are a far smaller exposure than inline scripts.
cspUnsafeEvalbooleanno`CSP` permits `unsafe-eval`.
cspUnsafeInlinebooleanno`CSP` permits `unsafe-inline` (a common XSS-defeating loosening).
cspWildcardbooleanno`CSP` uses a wildcard source (`*`) in a fetch directive.
gradestringnoA..F letter grade over the weighted quality checks below.
hstsbooleanno`Strict-Transport-Security` present at all.
hstsIncludeSubdomainsbooleanno`HSTS` carries `includeSubDomains`.
hstsMaxAgeinteger (int64)no`HSTS` `max-age` in seconds (0 when absent/unparseable).
httpsbooleannoWhether the audited target was reached over HTTPS.
notesarray<string>noHuman-readable quality issues (folded into the report findings).
permissionsPolicybooleanno`Permissions-Policy` (feature policy) present.
referrerPolicybooleanno`Referrer-Policy` present.
xContentTypeOptionsbooleanno`X-Content-Type-Options: nosniff`.
xFrameOptionsbooleanno`X-Frame-Options` present (clickjacking defense; CSP `frame-ancestors` is the modern form but XFO is still widely honoured).

SeoAnalysisMode

Type: string one of "source-html"

SeoCategory

Type: string one of "indexability", "searchPresentation", "siteLinkHygiene", "structuredData", "content", "socialPreview", "aiReadiness", "trust", "performance", "other"

SeoCheck

One stable, machine-comparable technical SEO check.

Properties
NameTypeRequiredDescription
categorySeoCategoryno
evidencearray<string>no
idstringno
messagestringno
recommendationstringno
severitySeoSeverityno
statusSeoCheckStatusno

SeoCheckChange

One stable technical SEO check's transition between comparable snapshots.

Properties
NameTypeRequiredDescription
afterStatusnull | SeoCheckStatusno
beforeStatusnull | SeoCheckStatusno
idstringyes
kindstringyes`introduced` / `resolved` / `status-changed`.

SeoCheckStatus

Type: string one of "pass", "fail", "warning", "opportunity", "notApplicable"

SeoIndexPresence

Actual presence in a public search engine's index. Source-HTML Site Audit has no authenticated engine evidence, so it must never infer this value from HTTP, robots, canonical, or sitemap signals.

Type: string one of "notMeasured"

SeoIndexability

Technical indexing eligibility inferred from fetched page signals. In particular, `Indexable` does not mean a search engine has indexed the URL.

Type: string one of "indexable", "excluded", "blocked", "unknown"

SeoMetrics

Advisory source-content metrics. None of these counts is a ranking factor or directly changes the technical score.

Properties
NameTypeRequiredDescription
emptyAnchorCountintegerno
emptyHeadingCountintegerno
externalLinkCountintegerno
hasBylinebooleanno
headingCountintegerno
headingOrderSkipsintegerno
hreflangCountintegerno
imageCountintegerno
imagesMissingAltintegerno
internalLinkCountintegerno
paragraphCountintegerno
visibleDateCountintegerno
wordCountintegerno

SeoReport

Separately versioned technical search-readiness result. This is not a ranking score and never alters the legacy `AuditReport.score`.

Properties
NameTypeRequiredDescription
analysisModeSeoAnalysisModeno
checksarray<SeoCheck>no
completebooleanno
expectedIndexablebooleanno
indexPresenceSeoIndexPresenceno
indexabilitySeoIndexabilityno
metricsSeoMetricsno
scoreinteger | null (int32)no
scoreModelVersioninteger (int32)no
sitemapSeoSitemapSummaryno
socialPreviewSocialPreviewSummaryno
structuredDataStructuredDataSummaryno

SeoSeverity

Type: string one of "high", "medium", "low", "info"

SeoSitemapSummary

Compact, additive sitemap evidence for the audited page. This describes the already-fetched conventional sitemap documents only; it does not claim a whole-site crawl when an index or another declared sitemap was not fetched.

Properties
NameTypeRequiredDescription
completebooleanno
documentsEvaluatedintegerno
entriesEvaluatedintegerno
membershipSitemapMembershipno

SiteConfig

Site configuration for crawling. Equivalent to Kotlin `SiteProfile.Config`.

Properties
NameTypeRequiredDescription
allowUrlWithQuerybooleanno
pageBodyCssSelectorstringno
sitemapsOnlybooleanno
urlstring | nullnoURL for the site configuration. Serializes as empty string if None.

SiteCreation

Site creation response. Equivalent to Kotlin `SiteCreation`.

Properties
NameTypeRequiredDescription
siteIdstring (uuid)yes
siteSecretstring (uuid)yes

Used by: POST /sites

SiteIndexSummary

Site index summary response. Equivalent to Kotlin `SiteIndexSummary`.

Properties
NameTypeRequiredDescription
documentsarray<string>yes
failedarray<string>yes
siteIdstring (uuid)yes
siteSecretstring (uuid)yes
successCountintegeryes

Used by: POST /sites/rss, PUT /sites/{siteId}/rss, PUT /sites/{siteId}/xml

SitePage

Page content for search indexing. Equivalent to Kotlin `SitePage`.

Properties
NameTypeRequiredDescription
bodystringyes
labelsarray<string>no
thumbnailstringyes
titlestringyes
urlstringyes

SitePageInput

Input DTO for page upsert (PUT /sites/{siteId}/pages). Matches the Kotlin `SitePage` input structure with optional fields.

Properties
NameTypeRequiredDescription
bodystring | nullno
idstring | nullno
labelsarray<string>no
siteIdstring | null (uuid)no
thumbnailstring | nullno
titlestring | nullno
updatedstring | nullno
urlstring | nullno

Used by: PUT /sites/{siteId}/pages, PUT /sites/{siteId}/pages/{pageId}

SiteProfile

Site profile configuration. Equivalent to Kotlin `SiteProfile`.

Properties
NameTypeRequiredDescription
configsarray<SiteConfig>no
emailstringno
idstring (uuid)yes
secretstring (uuid)yes

Used by: GET /sites/{siteId}/profile, PUT /sites/{siteId}/profile

SiteProfileConfigInput

Site profile config input (matching Kotlin's SiteProfile.Config for input)

Properties
NameTypeRequiredDescription
allowUrlWithQuerybooleanno
pageBodyCssSelectorstringno
sitemapsOnlybooleanno
urlstringno

SiteProfileCreation

Site profile creation request.

Properties
NameTypeRequiredDescription
configsarray<SiteProfileConfigInput>no
emailstringno

Used by: POST /sites

SiteProfileUpdate

Site profile update request. Matches Kotlin `SiteProfileUpdate` which includes secret for updating credentials.

Properties
NameTypeRequiredDescription
configsarray<SiteProfileConfigInput>no
emailstring | nullno
secretstring | null (uuid)noNew secret for the site (can be used to rotate credentials)

Used by: PUT /sites/{siteId}/profile

SitemapMembership

Whether the audited page's effective URL was observed in the bounded sitemap evidence available to the one-page audit. `Unknown` is deliberately distinct from `Absent`: only a complete, valid sitemap corpus can prove absence.

Type: string one of "present", "absent", "duplicate", "unknown"

SitesCrawlStatus

Collection of crawl statuses. Equivalent to Kotlin `SitesCrawlStatus`.

Properties
NameTypeRequiredDescription
sitesarray<CrawlStatus>yes

Used by: POST /sites/crawl, GET /sites/crawl/status, PUT /sites/crawl/status

SnapshotDiff

Properties
NameTypeRequiredDescription
afterSnapshotMetayes
beforeSnapshotMetayes
cdnChangenull | CdnChangeno
changesarray<UrlChange>yes
scoreDeltainteger (int32)yes`after.score - before.score`.
seoCheckChangesarray<SeoCheckChange>noStable-ID SEO check transitions. `None` means the snapshots' SEO analyses are not comparable; `Some([])` means they are comparable and no check status changed.
seoScoreDeltainteger | null (int32)no`after.seoScore - before.seoScore`, only for complete scored reports using the same SEO model, expectation, and analysis mode. Old or incomparable snapshots omit it.
unchangedintegeryesCommon paths whose probe was byte-for-byte unchanged.

Used by: GET /audit/diff

SnapshotMeta

Properties
NameTypeRequiredDescription
capturedAtinteger (int64)yes
hoststringyes
idstringyes
labelstring | nullno
scoreinteger (int32)yes
seoScoreinteger | null (int32)noTechnical SEO score, when this snapshot contains a complete scored SEO analysis. Kept separate from the frozen compatibility score.
seoScoreModelVersioninteger | null (int32)no
targetstringyes

SocialPreviewSummary

Properties
NameTypeRequiredDescription
openGraphDescriptionbooleanno
openGraphImagebooleanno
openGraphTitlebooleanno
openGraphUrlbooleanno
twitterCardbooleanno
twitterDescriptionbooleanno
twitterImagebooleanno
twitterTitlebooleanno

StartEmailRequest

Properties
NameTypeRequiredDescription
emailstringyes

Used by: POST /audit/watches/{id}/notifications/email

Stats

Properties
NameTypeRequiredDescription
analyticsStatsnull | AnalyticsStatsSummaryno
buildNumberstringyesCI build number injected via `BUILD_NUMBER` env var at runtime. CODEX Directive 3 contract: field is named `buildNumber` in public JSON. Pinned by `api.hurl:77`.
retentionSweepnull | SweepSummaryno
scmHashstringyesSource-control hash (git commit) injected via `SCM_HASH` env var at runtime. CODEX Directive 3 contract: this field is named `scmHash` in the public JSON. Do not rename. `api.hurl:78` pins this as a published API promise.
statusstringyesService status — always `"ok"` if this endpoint responds.
uptimeSinteger (int64)yesProcess uptime in seconds since api started.
versionstringyesCrate version (from Cargo manifest).

Used by: GET /stats.json

StoredSnapshot

An immutable, labelled audit snapshot.

Properties
NameTypeRequiredDescription
capturedAtinteger (int64)yesCapture time, epoch milliseconds.
idstringyes
labelstring | nullno
reportAuditReportyes

Used by: POST /audit/snapshots, GET /audit/snapshots/{id}

StructuredDataSummary

Properties
NameTypeRequiredDescription
articleItemsintegerno
articleOpportunitiesarray<string>noMissing recommended Article-family properties, surfaced as opportunities.
articleWarningsarray<string>noMalformed values for present Article-family properties. Missing recommended properties are tracked separately as advisory opportunities.
blocksintegerno
breadcrumbIssuesarray<string>noObjective BreadcrumbList structure errors (required properties/order).
breadcrumbListsintegerno
commerceOpportunitiesarray<string>noMissing context-dependent rich-result property groups. Organization has no universally required group and therefore creates no omission issue.
commerceWarningsarray<string>noMalformed values on present Product, SoftwareApplication, Organization, or Offer properties. These remain advisory so adding the validation does not alter the versioned score model.
invalidBlocksintegerno
invalidBreadcrumbListsintegerno
itemsintegerno
offerItemsintegerno
offerVisibilityWarningsarray<string>noMaterial Offer literals observed only in strongly hidden source nodes.
organizationItemsintegerno
pageUrlSignalsTruncatedbooleannoTrue when more distinct current-page URL signals were present than can be retained safely. The parent report is then incomplete and unscored.
productItemsintegerno
softwareApplicationItemsintegerno
truncatedbooleannoTrue when the bounded JSON-LD walk reached its nesting ceiling. The parent SEO report is then incomplete and deliberately unscored.
typesarray<string>no
validBlocksintegerno

SweepSummary

Published on `/stats.json` as `retentionSweep` so the `redis-capacity` alarm can tell a stalled sweep - the only thing bounding visit growth - from a healthy one.

Properties
NameTypeRequiredDescription
errorstring | nullno
lastOkUnixMsinteger | null (int64)no
lastRunUnixMsinteger (int64)yes
purgedintegeryes
sitesFailedintegeryes
truncatedbooleanyes

TlsFailureReason

Sanitized machine-readable TLS evidence; never parsed from URL-bearing errors.

Type: string one of "expired", "notYetValid", "hostMismatch", "unknownIssuer", "certificateInvalid", "dnsFailure", "connectFailure", "handshakeFailure", "timeout", "invalidServerName", "malformedCertificate", "admissionRejected" …

TlsInfo

TLS handshake + leaf-certificate facts for an HTTPS origin, captured by a dedicated pinned handshake (see [`crate::audit::tls`]). A tractable subset of what SSL Labs reports — enough for QA (expiry, key strength, hostname match, protocol support) without the heavy machinery (full cipher enumeration, per-client handshake simulation, active vulnerability probes) tracked in the backlog. Deliberately value-stable (no per-connection nonces) so re-audits diff cleanly. Absent for `http://` targets or when the handshake failed.

Properties
NameTypeRequiredDescription
alpnProtocolstring | nullnoThe ALPN protocol the server selected when offered `h2` and `http/1.1` (`h2` means HTTP/2). Absent when the server selected none, which means HTTP/1.1, and in reports captured before the handshake offered ALPN.
capturedAtinteger | null (int64)noCapture time for deterministic validity classification of retained reports.
chainarray<ChainCert>noThe additional certificates beyond the leaf (intermediates / roots).
chainLengthintegernoNumber of certificates the server presented (leaf + intermediates).
cipherSuitestring | nullnoThe negotiated cipher suite (e.g. `TLS13_AES_256_GCM_SHA384`).
configurationGradestring | nullnoProtocol/configuration grade independent of certificate trust, hostname, and validity. `grade` remains the legacy combined result.
daysRemaininginteger (int64)noWhole days until `notAfter` (negative once expired), from capture time.
gradestring | nullnoA conservative TLS-config letter grade (A..F) over the subset we test — NOT the full Qualys rating (which needs cipher/legacy-protocol/vuln probing; see the backlog).
hostMatchboolean | nullnoWhether the leaf covers the audited hostname (SAN, wildcard-aware).
issuerstring | nullnoIssuer common-name / organization, best-effort.
keyBitsinteger | null (int32)noLeaf public-key strength in bits (RSA modulus / EC field size).
keyTypestring | nullnoLeaf public-key type: `EC` / `RSA` / `Ed25519`.
notAfterinteger (int64)noThe leaf certificate's `notAfter`, epoch **seconds**.
notAfterUtcstring | nullno
notBeforeinteger (int64)noThe leaf certificate's `notBefore`, epoch **seconds**.
notBeforeUtcstring | nullno
protocolstring | nullnoThe highest TLS version negotiated (e.g. `TLSv1.3`).
sansarray<string>noDNS and IP Subject Alternative Names on the leaf, rendered as strings.
sha256string | nullno
signatureAlgorithmstring | nullnoLeaf certificate signature algorithm (e.g. `ecdsa-with-SHA384`).
subjectstring | nullnoSubject common-name, best-effort.
supportedProtocolsarray<string>noWhich TLS versions completed a handshake (subset of `TLSv1.2`,`TLSv1.3`).
trustFailureReasonnull | TlsFailureReasonno
trustStateTlsTrustStateno
validNowbooleannoTrue when the certificate is currently inside its validity window.

TlsInspectionEvidence

Scope of one dedicated TLS inspection, independent of the HTTP response.

Properties
NameTypeRequiredDescription
addressstring | nullno
capturedAtinteger (int64)yes
completebooleanyes
failureReasonnull | TlsFailureReasonno
hoststringyes
portinteger (int32)yes

TlsTrustState

Whether the presented certificate chain is trusted for the audited hostname by the bundled public WebPKI roots at capture time.

Type: string one of "trusted", "untrusted", "unknown"

TlsWatchStatus

Public latest-run evidence excludes the private notification dedup state.

Properties
NameTypeRequiredDescription
atinteger (int64)yes
hoststringyes
httpTlsFailurebooleanyes
inspectionnull | TlsInspectionEvidenceno
tlsnull | TlsInfono

Used by: GET /audit/watches/{id}/tls

TransportEvidence

How the page travels: negotiated HTTP version, advertised HTTP/3, and compression.

Properties
NameTypeRequiredDescription
alpnProtocolstring | nullnoThe ALPN protocol the TLS inspection handshake negotiated after offering `h2` and `http/1.1`. Absent for `http://`, a failed handshake, or a server that selects no protocol (which means HTTP/1.1).
altSvcProtocolsarray<string>noProtocol ids the page response's `Alt-Svc` header advertises, in order. The header itself is not reported, so its per-response parameters never show up as a header change in snapshot diffs.
compressionnull | CompressionEvidenceno
http3AdvertisedbooleannoThe page response's `Alt-Svc` header advertises HTTP/3 (`h3` or a draft `h3-*` id).

TrendSample

One point on a watch's score trend.

Properties
NameTypeRequiredDescription
atinteger (int64)yesWhen the run started, epoch milliseconds.
scoreinteger (int32)yesThe run's legacy compatibility score.

Used by: GET /audit/watches/{id}/trend

TrialStatus

Properties
NameTypeRequiredDescription
activeWatchesintegeryes
allowedHostsarray<string>yes
automaticTlsChecksEnabledbooleanyesWhether this deployment enables automatic trial TLS checks.
dailyReportLimitinteger (int32)yes
expiresAtinteger (int64)yesExclusive access expiry, in epoch milliseconds.
issuedAtinteger (int64)yesIssuance time, in epoch milliseconds (storage uses epoch seconds).
nextResetAtinteger (int64)yesNext UTC midnight, in epoch milliseconds.
remainingTodayinteger (int32)yes
trialIdstringyes
watchLimitintegeryes

Used by: GET /audit/trial

UpdateAsset

JSON body to edit an asset in place. Every field is optional — only the provided fields change. A `description` of `""` clears it; omitting it leaves it untouched. Ids/owner/createdAt are immutable.

Properties
NameTypeRequiredDescription
bookingFieldsarray<BookingFieldDef>noReplace the whole custom-field set (send `[]` to clear; omit to leave as-is).
currencystring | nullno
descriptionstring | nullno
kindstring | nullno
namestring | nullno
policynull | BookingPolicyno
promoCodesarray<PromoCode>noReplace the whole promo-code set (send `[]` to clear; omit to leave as-is).
timezonestring | nullno

Used by: PATCH /assets/owners/{ownerId}/assets/{assetId}

UpdateWindow

JSON body to edit a window's `cost` and/or `capacity` in place (its time range is immutable — delete + re-add, or reschedule bookings, to move a slot). `capacity` may not drop below the window's current active-booking count.

Properties
NameTypeRequiredDescription
capacityinteger | null (int32)no
costnumber | null (double)no

Used by: PATCH /assets/owners/{ownerId}/assets/{assetId}/windows/{windowId}

UrlChange

One URL's change between two snapshots.

Properties
NameTypeRequiredDescription
afterClassstring | nullno
beforeClassstring | nullno
detailsarray<string>no
kindstringyes`appeared` / `disappeared` / `broke` / `fixed` / `redirected` / `content-changed` / `header-changed` / `size-changed` / `status-changed`.
pathstringyes

User

Properties
NameTypeRequiredDescription
idinteger (int64)yesUnique user ID
usernamestringyesUsername

VerifyEmailRequest

Properties
NameTypeRequiredDescription
tokenstringyes

Used by: POST /audit/watches/{id}/notifications/email/verify

ViewerAsset

An asset returned to a read-only Viewer. Promo codes are anonymous discount capabilities and therefore stay on root/operational surfaces.

Properties
NameTypeRequiredDescription
bookingFieldsarray<BookingFieldDef>no
createdAtinteger (int64)yes
currencystringyes
descriptionstring | nullno
idstringyes
kindstringyes
namestringyes
policynull | BookingPolicyno
timezonestringyes

ViewerAssetWithWindows

Properties
NameTypeRequiredDescription
assetViewerAssetyes
windowCountinteger (int32)yes
windowsarray<ViewerWindow>yes

ViewerReservation

A booking visible to a read-only manager. All operational booking data is retained except the reservation bearer that can mutate the booking.

Properties
NameTypeRequiredDescription
assetIdstringyes
consumerstringyes
costnumber (double)yes
currencystringyes
fieldsmap<string, string>no
holdExpiresAtinteger | null (int64)no
notestring | nullno
quantityinteger (int32)yes
reservedAtinteger (int64)yes
statusReservationStatusyes
windowIdstringyes

ViewerReservationRow

Properties
NameTypeRequiredDescription
assetIdstringyes
assetNamestringyes
endinteger (int64)yes
reservationViewerReservationyes
startinteger (int64)yes
windowIdstringyes

ViewerWindow

A manager window whose embedded reservations cannot be used as public cancel/confirm/reschedule/modify capabilities.

Properties
NameTypeRequiredDescription
assetIdstringyes
capacityinteger (int32)yes
costnumber (double)yes
endinteger (int64)yes
idstringyes
remaininginteger (int32)yes
reservationsarray<ViewerReservation>yes
startinteger (int64)yes
waitlistedinteger (int32)yes

ViewerWindowsPage

Properties
NameTypeRequiredDescription
offsetinteger (int32)yes
totalinteger (int32)yes
windowsarray<ViewerWindow>yes

Visit

`/visit.json` verdict.

Properties
NameTypeRequiredDescription
asNamestringyesNetwork owner (ASN org) — explains datacenter / proxy bot verdicts.
asNumberinteger (int32)yes
countrystringyes
countryCodestringyes
ipstringyesClient IP used for the verdict (Envoy-derived, or `queryIP` override).
reasonsarray<string>yesHuman-readable signals behind the score.
scoreinteger (int32)yesUnified bot-likelihood, 0 (human) .. 100 (bot).
verdictstringyes`human` | `suspect` | `bot`.

Used by: POST /visit.json

VisitDay

One UTC day's counts for the time series.

Properties
NameTypeRequiredDescription
botinteger (int64)yes
daystringyes`YYYY-MM-DD` (UTC).
humaninteger (int64)yes
visitsinteger (int64)yes

VisitStats

Aggregated visit analytics for one tracked site over a window — returned by [`Metering::visit_stats`] / `GET /visit/stats.json`.

Properties
NameTypeRequiredDescription
byDayarray<VisitDay>yes
engagementnull | EngagementAggregateno
outboundnull | OutboundAggregateno
sincestringyesRFC3339 lower bound of the window.
siteIdstringyesThe tracked site id (serialized as `siteId`).
topBrowsersarray<CountRow>yesTop visitor browsers in the window, classified from the User-Agent (descending), capped at [`TOP_N`].
topCitiesarray<CountRow>yesTop visitor cities in the window (descending), capped at [`TOP_N`]; unknown (unresolved) cities are excluded.
topCountriesarray<CountRow>yesTop visitor countries (GeoIP ISO codes) in the window (descending), capped at [`TOP_N`]; unknown (unresolved) countries are excluded.
topDevicesarray<CountRow>yesTop visitor device types (Desktop / Mobile / Tablet) in the window, classified from the User-Agent (descending), capped at [`TOP_N`].
topJa3array<CountRow>yesTop TLS fingerprints (JA3) in the window — a strong automation signal. Aggregate only, with a [`JA3_MIN_VISITORS`] k-anonymity floor; empties excluded. Shown as-is (not pseudonymised) to stay threat-intel-matchable.
topNetworksarray<CountRow>yesTop visitor networks (GeoIP ASN organization) in the window (descending), capped at [`TOP_N`]; unknown (unresolved) networks are excluded.
topPagesarray<CountRow>yesMost-visited paths in the window (descending), capped at [`TOP_N`].
topReferrersarray<CountRow>yesTop external referrer hosts in the window (descending), capped at [`TOP_N`]; direct / same-site (empty-referrer) visits are excluded.
topSystemsarray<CountRow>yesTop visitor operating systems in the window, classified from the User-Agent (descending), capped at [`TOP_N`].
totalsVisitTotalsyes
vitalsnull | VitalsAggregateno

VisitStatsResponse

HTTP analytics response: aggregates plus current per-request configuration. Configuration must stay outside the report cache.

Type: VisitStats | object

Used by: GET /visit/stats.json

VisitTotals

Verdict totals for one site over a window.

Properties
NameTypeRequiredDescription
botinteger (int64)yes
humaninteger (int64)yes
newVisitsinteger (int64)yesVisits by a first-time identified visitor (Option B `loxal_vid` cookie) in the window — the "new" side of the new-vs-returning split. Zero for cookieless (Option A) sites, which issue no visitor id.
relayinteger (int64)yesVisits via iCloud Private Relay (privacy-preserving real humans).
returningVisitsinteger (int64)yesVisits by a visitor seen before (Option B cookie) in the window — the "returning" side of the split.
suspectinteger (int64)yes
torinteger (int64)yesVisits over Tor exit nodes (anonymized) in the window.
uniqueVisitorsinteger (int64)yesDistinct visitors (by client IP, cookieless) in the window.
visitsinteger (int64)yesTotal page views — one per recorded beacon — in the window.

VitalsAggregate

Aggregate Core Web Vitals over the same window as [`VisitStats`]. Absent (`None`) when the window contains zero opt-in samples for a metric. `P75` is Google's canonical field-metric summary — it reflects the 75th-percentile experience across all sampled visits, which is what Search Console reports.

Properties
NameTypeRequiredDescription
p75Clsnumber | null (double)noP75 Cumulative Layout Shift (unitless) — Google: < 0.1 good, < 0.25 needs improvement, otherwise poor.
p75InpMsnumber | null (double)noP75 Interaction to Next Paint (ms) — Google: < 200 good, < 500 needs improvement, otherwise poor.
p75LcpMsnumber | null (double)noP75 Largest Contentful Paint (ms) — Google: < 2500 good, < 4000 needs improvement, otherwise poor.
p75TtfbMsnumber | null (double)noP75 Time to First Byte (ms) — Google (informational): < 800 good, < 1800 needs improvement.
samplesinteger (int64)yesTotal number of visits in the window that contributed at least one vitals metric. When zero, the four P75 fields are all `None`.

VoucherQuotaStatus

`GET /audit/quota`: the paid reports a Site Audit voucher has left.

Properties
NameTypeRequiredDescription
remaininginteger (int32)yesPaid reports the voucher can start now; credits reserved by audits in progress are not counted.
totalinteger (int32)yesPaid reports the voucher was issued with.

Used by: GET /audit/quota

WaitlistEntry

A consumer's place in a window's waitlist. `id` is their capability to leave the queue. When a booking on the window is cancelled and frees a seat, the head of the queue is auto-promoted into a [hold] and a `waitlist.promote` event fires (so the operator can act on it). [hold]: AssetStore::hold

Properties
NameTypeRequiredDescription
assetIdstringyes
consumerstringyes
fieldsmap<string, string>noCustom booking-field values carried onto the promoted hold (empty = none).
idstringyes
joinedAtinteger (int64)yes
notestring | nullno
quantityinteger (int32)noSeats to promote into when this ticket reaches the head (default 1). The head is only promoted when at least this many seats free.
windowIdstringyes

Used by: POST /assets/asset/{assetId}/windows/{windowId}/waitlist

WatchAlert

A regression alert recorded (and optionally webhook'd) for a watch.

Properties
NameTypeRequiredDescription
atinteger (int64)yesWhen the run that raised the alert started, epoch milliseconds.
kindstringyes`score-drop` / `score-below-minimum` / `seo-score-drop` / `broke` / `cdn-removed` / `unreachable` / `cert-expiry`.
messagestringyesWhat regressed, in words.
scoreAfterinteger | null (int32)noThe legacy compatibility score of the run that raised the alert.
scoreBeforeinteger | null (int32)noThe legacy compatibility score before the regression, when known.

Used by: GET /audit/watches/{id}/alerts

WatchMode

Full audits remain the default for old stored watches and existing clients.

Type: string one of "full", "tls"

Used by: POST /audit/watches

Webhook

An outbound event webhook: the owner asks us to POST a JSON payload to `url` whenever a matching event fires on their assets. `id` doubles as the delete capability. `url` is SSRF-guarded at registration *and* at send.

Properties
NameTypeRequiredDescription
createdAtinteger (int64)yes
eventsarray<string>yesEvent selectors: `"*"` (all), a category (`booking`/`asset`/`window`/ `owner`), or an exact action (`reserve`, `cancel`, `asset.create`, …).
idstringyes
urlstringyesSanitized target URL (http/https, web port, credentials stripped).

Used by: GET /assets/owners/{ownerId}/webhooks, POST /assets/owners/{ownerId}/webhooks

WhoAmI

The resolved identity behind a manager-surface token — lets a client tailor its UI to the caller's authority without leaking the owner secret.

Properties
NameTypeRequiredDescription
isOwnerbooleanyesTrue iff the caller holds the root ownerId.
roleRoleyes

Used by: GET /assets/owners/{ownerId}/whoami

Whois

Whois response schema — stable across all data-source backends. **Why all 12 fields are present on every response, even when 5 of them are not populated under the current default backend:** - The default data source is DB-IP Lite (CC BY 4.0). It carries 7 of these 12 fields with real values; the remaining 5 (`code` postal, `cityId`, `countryState`, `countryStateId`, `timeZone`) come back with the `EXTENDED_FIELD_HINT` / `EXTENDED_FIELD_HINT_ID` sentinel — telling the consumer the field is part of the schema but only populated for Pro / Enterprise customers. - The 5 hinted fields are positioned to customers as **extended fields available on request** for Pro / Enterprise tiers (see `sites/www/product/ip-intelligence.html`, which also hosts the folded `#pricing` section). When a paying customer triggers a MaxMind GeoIP2 license purchase, the data-source path for that key fills in those fields with real values — no schema change required, no consumer breakage. CODEX Directive 3 wins via the response shape staying constant.

Properties
NameTypeRequiredDescription
asNamestringyesAutonomous System name (ISP/organization)
asNumberinteger (int32)yesAutonomous System number
citystringyesCity name
cityIdinteger (int64)yesGeoNames city ID. **Extended field** — `-1` (`EXTENDED_FIELD_HINT_ID`) under the default backend; real positive integer on the upgrade.
codestringyesPostal/ZIP code. **Extended field** — populated for Pro / Enterprise customers with the paid MaxMind GeoIP2 backend. Default backend returns the [`EXTENDED_FIELD_HINT`] sentinel.
countrystringyesCountry name
countryCodestringyesISO 3166-1 alpha-2 country code
countryIdinteger (int64)yesGeoNames country ID
countryStatestringyesState / province ISO code (e.g. "NSW"). **Extended field** — see [`EXTENDED_FIELD_HINT`].
countryStateIdinteger (int64)yesGeoNames state / province ID. **Extended field** — see [`EXTENDED_FIELD_HINT_ID`].
ipstringyesClient IP address
timeZonestringyesIANA time zone identifier. **Extended field** — see [`EXTENDED_FIELD_HINT`].

Used by: GET /whois.json

Window

A utilization window as returned to the manager: the slot plus its live booking state (`remaining` seats + the reservations on it).

Properties
NameTypeRequiredDescription
assetIdstringyes
capacityinteger (int32)yesHow many can book this slot (default 1).
costnumber (double)yesCost to use the asset for this window, in the asset's `currency`.
endinteger (int64)yesSlot end, epoch-ms UTC (exclusive); must be `> start`.
idstringyes
remaininginteger (int32)yesSeats still available = `capacity` − active bookings.
reservationsarray<Reservation>yesThe bookings on this window (active + cancelled; `status` distinguishes).
startinteger (int64)yesSlot start, epoch-ms UTC (inclusive).
waitlistedinteger (int32)noHow many consumers are queued on this window's waitlist.

Used by: POST /assets/owners/{ownerId}/assets/{assetId}/windows, PATCH /assets/owners/{ownerId}/assets/{assetId}/windows/{windowId}

WindowsPage

A page of an asset's windows (owner drill-down; `total` is the full count).

Properties
NameTypeRequiredDescription
offsetinteger (int32)yes
totalinteger (int32)yes
windowsarray<Window>yes

WindowsPageResponse

Role-dependent serialization for one asset's window page. Dispatchers need reservation IDs for their `Book` capability; viewers do not receive them.

Type: WindowsPage | ViewerWindowsPage

Used by: GET /assets/owners/{ownerId}/assets/{assetId}/windows