{"openapi":"3.1.0","info":{"title":"Lifub API","description":"api Web Utils for humans and bots","contact":{"name":"Lifub","url":"https://www.lifub.com"},"license":{"name":"MIT","url":"https://opensource.org/licenses/MIT"},"version":"1.0.3"},"servers":[{"url":"https://api.lifub.com","description":"Default"}],"paths":{"/admin/active-crawls":{"get":{"tags":["admin"],"summary":"Handler for GET /admin/active-crawls","description":"Returns the number of in-flight crawls plus the site IDs currently\nbeing crawled. Used by the §7.4 migration runbook to poll for\n`running == 0` after setting `MIGRATION_LOCKDOWN=true`.","operationId":"handle_active_crawls","responses":{"200":{"description":"Active crawl counter","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActiveCrawlsResponse"}}}},"401":{"description":"Unauthorized - invalid admin secret"}},"security":[{"admin_secret":[]}]}},"/admin/promoted":{"get":{"tags":["admin"],"summary":"Handler for GET /admin/promoted","description":"Reports whether the embedded store's warm-standby writer has been\narmed (`EmbeddedStore::promote`, see the `api` Deployment `strategy:`\ncomment in `api.yaml`). Unauthenticated by design — a pure operational\nliveness signal like `/health`, exposing no data.\n\nA warm-standby process answers `/health` while still read-only, before it\ntakes the SQLite writer flock. Deployment checks can poll this endpoint so\ncallers never race the promotion\nwindow into a raw 500 \"attempt to write a readonly database\"\n(`voyk/src/store/embedded/mod.rs::promote`).","operationId":"handle_promoted","responses":{"200":{"description":"Writer promoted; the store accepts writes"},"503":{"description":"Still warm-standby; retry after the given Retry-After"}}}},"/analytics.js":{"get":{"tags":["visit"],"summary":"Consolidated tracking tag (server record + client collector)","description":"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.","operationId":"handle_analytics_js","parameters":[{"name":"siteId","in":"query","description":"Site id to count the visit against","required":false,"schema":{"type":"string"}},{"name":"queryIP","in":"query","description":"Override IP for the geolocation / ASN lookup","required":false,"schema":{"type":"string"},"example":"1.1.1.1"},{"name":"cookies","in":"query","description":"Option B: enable the first-party loxal_vid cookie (returning-visitor detection)","required":false,"schema":{"type":"boolean"}},{"name":"banner","in":"query","description":"Option B + banner: serve Analytics' own equal-prominence consent UI","required":false,"schema":{"type":"boolean"}},{"name":"cookieConsent","in":"query","description":"Option B2 (`signal`): defer the cookie write until `window.loxalAnalytics.consent('granted')`","required":false,"schema":{"type":"string"}},{"name":"vitals","in":"query","description":"Opt-in Core Web Vitals collection (LCP / INP / CLS / TTFB), posted to /visit/vitals.json on visibilitychange","required":false,"schema":{"type":"boolean"}},{"name":"engagement","in":"query","description":"Opt-in engagement-depth collection (time on page, scroll %, interaction count), posted to /visit/engagement.json on visibilitychange","required":false,"schema":{"type":"boolean"}},{"name":"outbound","in":"query","description":"Opt-in outbound-clicks & downloads collection (external <a> hosts, download file extensions), posted to /visit/outbound.json on visibilitychange","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"Collector JavaScript (text/javascript)"}}}},"/assets/asset/{assetId}":{"get":{"tags":["assets"],"summary":"Public consumer view of an asset and its bookable windows","operationId":"handle_public_view","parameters":[{"name":"assetId","in":"path","description":"Shareable asset id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Asset with availability","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetView"}}}},"404":{"description":"Unknown asset"}}}},"/assets/asset/{assetId}/calendar.ics":{"get":{"tags":["assets"],"summary":"Public availability calendar for one asset as iCalendar (.ics)","operationId":"handle_public_calendar","parameters":[{"name":"assetId","in":"path","description":"Shareable asset id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"text/calendar","content":{"text/calendar":{}}},"404":{"description":"Unknown asset"}}}},"/assets/asset/{assetId}/windows/{windowId}/hold":{"post":{"tags":["assets"],"summary":"Tentatively hold a seat that auto-releases after ttlSeconds (atomic, capacity-aware)","operationId":"handle_hold","parameters":[{"name":"assetId","in":"path","description":"Shareable asset id","required":true,"schema":{"type":"string"}},{"name":"windowId","in":"path","description":"Window to hold a seat on","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewHold"}}},"required":true},"responses":{"201":{"description":"Held booking (confirm it with its id before it expires)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reservation"}}}},"400":{"description":"Invalid ttlSeconds"},"404":{"description":"Unknown asset or window"},"409":{"description":"No seats left (slot_full)"}}}},"/assets/asset/{assetId}/windows/{windowId}/reserve":{"post":{"tags":["assets"],"summary":"Reserve/book a utilization window (atomic, capacity-aware)","operationId":"handle_reserve","parameters":[{"name":"assetId","in":"path","description":"Shareable asset id","required":true,"schema":{"type":"string"}},{"name":"windowId","in":"path","description":"Window to reserve","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewReservation"}}},"required":true},"responses":{"201":{"description":"Booking receipt (id doubles as the cancel capability)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reservation"}}}},"404":{"description":"Unknown asset or window"},"409":{"description":"No seats left (slot_full)"}}}},"/assets/asset/{assetId}/windows/{windowId}/waitlist":{"post":{"tags":["assets"],"summary":"Join a window's waitlist; the head is auto-promoted when a seat frees","operationId":"handle_join_waitlist","parameters":[{"name":"assetId","in":"path","description":"Shareable asset id","required":true,"schema":{"type":"string"}},{"name":"windowId","in":"path","description":"Window to queue for","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewWaitlist"}}},"required":true},"responses":{"201":{"description":"Waitlist ticket (id is the leave capability)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaitlistEntry"}}}},"404":{"description":"Unknown asset or window"},"429":{"description":"Waitlist is full"}}}},"/assets/asset/{assetId}/windows/{windowId}/waitlist/{ticketId}":{"delete":{"tags":["assets"],"summary":"Leave a window's waitlist (ticketId is the capability)","operationId":"handle_leave_waitlist","parameters":[{"name":"assetId","in":"path","description":"Shareable asset id","required":true,"schema":{"type":"string"}},{"name":"windowId","in":"path","description":"Window the ticket is on","required":true,"schema":{"type":"string"}},{"name":"ticketId","in":"path","description":"Waitlist ticket to remove","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Left the waitlist"},"404":{"description":"Unknown ticket"}}}},"/assets/owners":{"post":{"tags":["assets"],"summary":"Mint a new ownerId (manager bearer secret) — Beta","operationId":"handle_mint_owner","responses":{"201":{"description":"Minted ownerId — `{ \"ownerId\": \"<uuid>\" }`"}}}},"/assets/owners/{ownerId}/assets":{"get":{"tags":["assets"],"summary":"List the owner's assets; ?windows=false for a lightweight summary","operationId":"handle_list_assets","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId","required":true,"schema":{"type":"string"}},{"name":"windows","in":"query","description":"false → metadata + windowCount only (no inlined windows)","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"Assets (with windows, or summary); delegated responses omit ownerId and Viewer responses omit reservation IDs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetRowsResponse"}}}},"404":{"description":"Unknown owner"}}},"post":{"tags":["assets"],"summary":"Create an asset","operationId":"handle_create_asset","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewAsset"}}},"required":true},"responses":{"201":{"description":"Created asset","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Asset"}}}},"404":{"description":"Unknown owner"}}}},"/assets/owners/{ownerId}/assets/{assetId}":{"delete":{"tags":["assets"],"summary":"Delete an asset and all its windows","operationId":"handle_delete_asset","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId","required":true,"schema":{"type":"string"}},{"name":"assetId","in":"path","description":"Asset to delete","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"description":"Unknown asset"}}},"patch":{"tags":["assets"],"summary":"Edit an asset's mutable fields in place (only provided fields change)","operationId":"handle_update_asset","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId","required":true,"schema":{"type":"string"}},{"name":"assetId","in":"path","description":"Asset to edit","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAsset"}}},"required":true},"responses":{"200":{"description":"Updated asset","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Asset"}}}},"400":{"description":"Invalid field"},"404":{"description":"Unknown asset"}}}},"/assets/owners/{ownerId}/assets/{assetId}/availability":{"post":{"tags":["assets"],"summary":"Bulk-generate a recurring grid of utilization windows","operationId":"handle_add_availability","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId","required":true,"schema":{"type":"string"}},{"name":"assetId","in":"path","description":"Asset the windows belong to","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewAvailability"}}},"required":true},"responses":{"201":{"description":"Generation result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailabilityResult"}}}},"400":{"description":"Invalid recurrence"},"404":{"description":"Unknown asset"}}}},"/assets/owners/{ownerId}/assets/{assetId}/windows":{"get":{"tags":["assets"],"summary":"A page of one asset's windows (owner drill-down)","operationId":"handle_list_asset_windows","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId","required":true,"schema":{"type":"string"}},{"name":"assetId","in":"path","description":"Asset to page windows for","required":true,"schema":{"type":"string"}},{"name":"offset","in":"query","description":"Start index (default 0)","required":false,"schema":{"type":"integer","format":"int32","minimum":0}},{"name":"limit","in":"query","description":"Page size (default 200, max 1000)","required":false,"schema":{"type":"integer","format":"int32","minimum":0}}],"responses":{"200":{"description":"A page of windows; Viewer responses omit reservation IDs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WindowsPageResponse"}}}},"404":{"description":"Unknown asset"}}},"post":{"tags":["assets"],"summary":"Add a utilization window (time slot with a cost + capacity)","operationId":"handle_add_window","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId","required":true,"schema":{"type":"string"}},{"name":"assetId","in":"path","description":"Asset the window belongs to","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewWindow"}}},"required":true},"responses":{"201":{"description":"Created window","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Window"}}}},"404":{"description":"Unknown asset"},"409":{"description":"Window overlaps an existing one"}}},"delete":{"tags":["assets"],"summary":"Bulk-delete an asset's windows whose start is in [from, to) (clear a date range)","operationId":"handle_bulk_delete_windows","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId","required":true,"schema":{"type":"string"}},{"name":"assetId","in":"path","description":"Asset to clear windows on","required":true,"schema":{"type":"string"}},{"name":"from","in":"query","description":"Range start, epoch-ms (inclusive)","required":true,"schema":{"type":"integer","format":"int64","minimum":0}},{"name":"to","in":"query","description":"Range end, epoch-ms (exclusive)","required":true,"schema":{"type":"integer","format":"int64","minimum":0}}],"responses":{"200":{"description":"Count deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkDeleteResult"}}}},"400":{"description":"Missing/invalid range"},"404":{"description":"Unknown asset"}}}},"/assets/owners/{ownerId}/assets/{assetId}/windows/{windowId}":{"delete":{"tags":["assets"],"summary":"Delete a utilization window","operationId":"handle_delete_window","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId","required":true,"schema":{"type":"string"}},{"name":"assetId","in":"path","description":"Asset the window belongs to","required":true,"schema":{"type":"string"}},{"name":"windowId","in":"path","description":"Window to delete","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"description":"Unknown asset or window"}}},"patch":{"tags":["assets"],"summary":"Edit a window's cost/capacity in place (time range immutable)","operationId":"handle_update_window","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId","required":true,"schema":{"type":"string"}},{"name":"assetId","in":"path","description":"Asset the window belongs to","required":true,"schema":{"type":"string"}},{"name":"windowId","in":"path","description":"Window to edit","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWindow"}}},"required":true},"responses":{"200":{"description":"Updated window","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Window"}}}},"400":{"description":"Invalid cost/capacity"},"404":{"description":"Unknown asset or window"},"409":{"description":"Capacity below current bookings"}}}},"/assets/owners/{ownerId}/audit":{"get":{"tags":["assets"],"summary":"The owner's activity log (audit trail), newest first","operationId":"handle_list_audit","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Audit events; delegated responses omit reservationId and free-form detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditRowsResponse"}}}},"404":{"description":"Unknown owner"}}}},"/assets/owners/{ownerId}/calendar.ics":{"get":{"tags":["assets"],"summary":"Owner's full schedule as an iCalendar (.ics) feed","operationId":"handle_owner_calendar","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"text/calendar","content":{"text/calendar":{}}},"404":{"description":"Unknown owner"}}}},"/assets/owners/{ownerId}/export":{"get":{"tags":["assets"],"summary":"Export the owner's full data snapshot (assets, bookings, members, webhooks) — owner only","operationId":"handle_export","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId (root)","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Portable snapshot","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OwnerExport"}}}},"403":{"description":"Only the owner may export"},"404":{"description":"Unknown owner"}}}},"/assets/owners/{ownerId}/import":{"post":{"tags":["assets"],"summary":"Import a data snapshot into an empty owner (ids preserved) — owner only","operationId":"handle_import","parameters":[{"name":"ownerId","in":"path","description":"A freshly-minted, empty ownerId (root)","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OwnerExport"}}},"required":true},"responses":{"201":{"description":"Import counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportResult"}}}},"400":{"description":"Invalid snapshot or unsupported export version"},"403":{"description":"Only the owner may import"},"404":{"description":"Unknown owner"},"409":{"description":"Target owner is not empty or a globally keyed id collides"}}}},"/assets/owners/{ownerId}/members":{"get":{"tags":["assets"],"summary":"List the owner's role-scoped member tokens (owner only)","operationId":"handle_list_members","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId (root)","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Members","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Member"}}}}},"403":{"description":"Only the owner may list members"},"404":{"description":"Unknown owner"}}},"post":{"tags":["assets"],"summary":"Mint a role-scoped member token (owner only)","operationId":"handle_mint_member","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId (root)","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewMember"}}},"required":true},"responses":{"201":{"description":"Minted member token (id is the bearer token + revoke handle)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Member"}}}},"400":{"description":"Invalid role/label"},"403":{"description":"Only the owner may mint members"},"404":{"description":"Unknown owner"},"429":{"description":"Per-owner member limit reached"}}}},"/assets/owners/{ownerId}/members/{memberId}":{"delete":{"tags":["assets"],"summary":"Revoke a member token (owner only)","operationId":"handle_delete_member","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId (root)","required":true,"schema":{"type":"string"}},{"name":"memberId","in":"path","description":"Member token to revoke","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Revoked"},"403":{"description":"Only the owner may revoke members"},"404":{"description":"Unknown owner or member"}}}},"/assets/owners/{ownerId}/report":{"get":{"tags":["assets"],"summary":"Owner utilization + revenue report (aggregation over all assets)","operationId":"handle_report","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Aggregated report","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OwnerReport"}}}},"404":{"description":"Unknown owner"}}}},"/assets/owners/{ownerId}/reservations":{"get":{"tags":["assets"],"summary":"List active bookings across the owner's assets (paginated)","operationId":"handle_list_reservations","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId","required":true,"schema":{"type":"string"}},{"name":"offset","in":"query","description":"Start index (default 0)","required":false,"schema":{"type":"integer","format":"int32","minimum":0}},{"name":"limit","in":"query","description":"Page size (default 200, max 1000)","required":false,"schema":{"type":"integer","format":"int32","minimum":0}}],"responses":{"200":{"description":"Reservations (page); Viewer responses omit reservation IDs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReservationRowsResponse"}}}},"404":{"description":"Unknown owner"}}}},"/assets/owners/{ownerId}/reservations/{reservationId}/cancel":{"post":{"tags":["assets"],"summary":"Owner cancels a booking on one of their assets (frees the seat)","operationId":"handle_owner_cancel","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId","required":true,"schema":{"type":"string"}},{"name":"reservationId","in":"path","description":"Booking to cancel","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cancelled booking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reservation"}}}},"404":{"description":"Unknown reservation"}}}},"/assets/owners/{ownerId}/reservations/{reservationId}/modify":{"patch":{"tags":["assets"],"summary":"Owner edits a booking on one of their assets in place (seat quantity, fields, note)","operationId":"handle_owner_modify","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId (or a member token)","required":true,"schema":{"type":"string"}},{"name":"reservationId","in":"path","description":"Booking to modify","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModifyReservation"}}},"required":true},"responses":{"200":{"description":"Updated booking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reservation"}}}},"400":{"description":"Invalid quantity/fields/note"},"404":{"description":"Unknown reservation"},"409":{"description":"Increase doesn't fit (slot_full) / booking not active"}}}},"/assets/owners/{ownerId}/webhooks":{"get":{"tags":["assets"],"summary":"List the owner's registered outbound webhooks","operationId":"handle_list_webhooks","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Registered webhooks","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}}}}},"404":{"description":"Unknown owner"}}},"post":{"tags":["assets"],"summary":"Register an outbound event webhook (SSRF-guarded, web ports only)","operationId":"handle_register_webhook","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewWebhook"}}},"required":true},"responses":{"201":{"description":"Registered webhook (id doubles as the delete capability)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Webhook"}}}},"400":{"description":"Invalid or disallowed URL / event"},"404":{"description":"Unknown owner"},"429":{"description":"Per-owner webhook limit reached"}}}},"/assets/owners/{ownerId}/webhooks/{webhookId}":{"delete":{"tags":["assets"],"summary":"Delete a registered webhook","operationId":"handle_delete_webhook","parameters":[{"name":"ownerId","in":"path","description":"Manager's secret ownerId","required":true,"schema":{"type":"string"}},{"name":"webhookId","in":"path","description":"Webhook to delete","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"description":"Unknown owner or webhook"}}}},"/assets/owners/{ownerId}/whoami":{"get":{"tags":["assets"],"summary":"Resolve the caller's effective role (owner or a member token)","operationId":"handle_whoami","parameters":[{"name":"ownerId","in":"path","description":"Owner secret or a member token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Resolved identity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhoAmI"}}}},"404":{"description":"Unknown owner/member token"}}}},"/assets/reservations/{reservationId}/cancel":{"post":{"tags":["assets"],"summary":"Consumer cancels their own booking (reservationId is the capability)","operationId":"handle_cancel_reservation","parameters":[{"name":"reservationId","in":"path","description":"The booking receipt id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cancelled booking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reservation"}}}},"404":{"description":"Unknown reservation"}}}},"/assets/reservations/{reservationId}/confirm":{"post":{"tags":["assets"],"summary":"Promote a held seat to a confirmed booking (reservationId is the capability)","operationId":"handle_confirm_reservation","parameters":[{"name":"reservationId","in":"path","description":"The held booking's receipt id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Confirmed booking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reservation"}}}},"404":{"description":"Unknown reservation"},"409":{"description":"Hold already expired, or the booking isn't a hold"}}}},"/assets/reservations/{reservationId}/modify":{"patch":{"tags":["assets"],"summary":"Consumer edits their active booking in place (seat quantity, custom fields, note)","operationId":"handle_modify_reservation","parameters":[{"name":"reservationId","in":"path","description":"The booking receipt id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModifyReservation"}}},"required":true},"responses":{"200":{"description":"Updated booking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reservation"}}}},"400":{"description":"Invalid quantity/fields/note"},"404":{"description":"Unknown reservation"},"409":{"description":"Increase doesn't fit (slot_full) / booking not active"}}}},"/assets/reservations/{reservationId}/reschedule":{"post":{"tags":["assets"],"summary":"Consumer reschedules their booking onto another window of the same asset","operationId":"handle_reschedule","parameters":[{"name":"reservationId","in":"path","description":"The booking receipt id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RescheduleRequest"}}},"required":true},"responses":{"201":{"description":"New booking (fresh reservationId)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reservation"}}}},"404":{"description":"Unknown reservation or target window"},"409":{"description":"Target full / already cancelled / same window"}}}},"/audit.json":{"get":{"tags":["audit"],"summary":"Audit one page's HTTP/security health and technical SEO readiness","description":"Fetches a public page and a bounded set of same-origin well-known files. 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.","operationId":"handle_audit","parameters":[{"name":"url","in":"query","description":"Origin/page URL to audit.","required":false,"schema":{"type":["string","null"]}},{"name":"format","in":"query","description":"`?format=junit` → JUnit XML instead of JSON (CI-friendly). Any other\nvalue (or absent) yields JSON.","required":false,"schema":{"type":["string","null"]}},{"name":"gate","in":"query","description":"`?gate=N` → a CI gate: respond `422 Unprocessable Entity` when the audit\nscore is below `N` (so `curl --fail` blocks a deploy), else `200`.","required":false,"schema":{"type":["integer","null"],"format":"int32","minimum":0}},{"name":"seoGate","in":"query","description":"`?seoGate=N` → independently gate the complete technical SEO score.\nMissing, interrupted, or otherwise unscored SEO analysis fails closed.","required":false,"schema":{"type":["integer","null"],"format":"int32","minimum":0}},{"name":"expectIndexable","in":"query","description":"Whether this URL is intended to be indexable. Defaults to `true`; set\n`expectIndexable=false` for deliberate noindex/excluded pages.","required":false,"schema":{"type":["boolean","null"]}},{"name":"share","in":"query","description":"`?share=true` → also publish this completed report under an unguessable\nid so it can be handed to someone else as a link, and return that id as\n`shareId`. Off by default: an automated or CI caller stores nothing.\nSharing never re-runs the audit and never spends an extra credit.","required":false,"schema":{"type":["boolean","null"]}}],"responses":{"200":{"description":"Audit completed; JUnit XML is returned when format=junit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditReport"}},"application/xml":{"schema":{"type":"string"}}}},"400":{"description":"URL is missing, malformed, or disallowed by the SSRF policy"},"402":{"description":"The supplied Site Audit voucher has no paid reports remaining"},"403":{"description":"The supplied Site Audit voucher is invalid or revoked"},"409":{"description":"Paid credits exist but are temporarily reserved by audits in progress; retry after the Retry-After interval"},"422":{"description":"The requested legacy gate or SEO gate failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditReport"}},"application/xml":{"schema":{"type":"string"}}}},"429":{"description":"Free one-hour quota, per-target, or anonymous safety limit exceeded"},"502":{"description":"Target DNS resolution failed"},"503":{"description":"The durable Site Audit quota backend is unavailable"}},"security":[{},{"audit_voucher":[]}]}},"/audit/crawl":{"get":{"tags":["audit"],"summary":"Run a bounded sampled site crawl","description":"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.","operationId":"site_audit_crawl","parameters":[{"name":"url","in":"query","required":false,"schema":{"type":["string","null"]}},{"name":"limit","in":"query","description":"Number of pages to audit, clamped to `1..=MAX_PAGES`.","required":false,"schema":{"type":["integer","null"],"minimum":0}},{"name":"expectIndexable","in":"query","description":"Whether sampled pages are intended to be indexable. Defaults to true.","required":false,"schema":{"type":["boolean","null"]}}],"responses":{"200":{"description":"Bounded crawl completed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlReport"}}}},"400":{"description":"URL is missing, malformed, or disallowed by the SSRF policy"},"429":{"description":"Per-target or anonymous request limit exceeded"},"502":{"description":"Target DNS resolution failed"}}}},"/audit/diff":{"get":{"tags":["audit"],"summary":"Compare two Site Audit snapshots","description":"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.","operationId":"handle_diff","parameters":[{"name":"before","in":"query","required":false,"schema":{"type":["string","null"]}},{"name":"after","in":"query","required":false,"schema":{"type":["string","null"]}},{"name":"format","in":"query","description":"`?format=junit` → JUnit XML instead of JSON (CI-friendly).","required":false,"schema":{"type":["string","null"]}},{"name":"gate","in":"query","description":"`?gate=N` → a CI gate: respond `422` when the score delta is below `N`\n(default 0 → any regression fails) or anything `broke`/`disappeared`.","required":false,"schema":{"type":["integer","null"],"format":"int32"}},{"name":"seoDeltaGate","in":"query","description":"`?seoDeltaGate=N` → respond `422` when comparable technical SEO score\nmovement is below `N`; unavailable/incomparable SEO fails closed.","required":false,"schema":{"type":["integer","null"],"format":"int32"}}],"responses":{"200":{"description":"Snapshot diff; JUnit XML is returned when format=junit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnapshotDiff"}},"application/xml":{"schema":{"type":"string"}}}},"400":{"description":"Both snapshot IDs are required"},"404":{"description":"A snapshot was not found"},"422":{"description":"A requested legacy or SEO delta gate failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnapshotDiff"}},"application/xml":{"schema":{"type":"string"}}}},"500":{"description":"Snapshot storage failed"}}}},"/audit/snapshots":{"post":{"tags":["audit"],"summary":"Capture an immutable Site Audit snapshot","operationId":"handle_create_snapshot","parameters":[{"name":"url","in":"query","required":false,"schema":{"type":["string","null"]}},{"name":"label","in":"query","required":false,"schema":{"type":["string","null"]}},{"name":"expectIndexable","in":"query","description":"Whether this URL is deliberately excluded from search indexing.","required":false,"schema":{"type":["boolean","null"]}},{"name":"seoGate","in":"query","description":"Optional technical SEO score gate. Incomplete/unscored analysis fails\nclosed and is not stored as a snapshot.","required":false,"schema":{"type":["integer","null"],"format":"int32","minimum":0}}],"responses":{"201":{"description":"Snapshot captured and stored","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoredSnapshot"}}}},"400":{"description":"URL is missing, malformed, or disallowed"},"402":{"description":"The supplied Site Audit voucher has no paid reports remaining"},"403":{"description":"The supplied Site Audit voucher is invalid or revoked"},"409":{"description":"Paid credits exist but are temporarily reserved by audits in progress; retry after the Retry-After interval"},"422":{"description":"The requested technical SEO gate failed; snapshot was not stored","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditReport"}}}},"429":{"description":"Free one-hour quota, per-target, or anonymous safety limit exceeded"},"500":{"description":"Snapshot storage failed"},"503":{"description":"The durable Site Audit quota backend is unavailable"}},"security":[{},{"audit_voucher":[]}]}},"/audit/snapshots/{id}":{"get":{"tags":["audit"],"summary":"Fetch a stored Site Audit snapshot","operationId":"handle_get_snapshot","parameters":[{"name":"id","in":"path","description":"Snapshot bearer-token ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Stored snapshot","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoredSnapshot"}}}},"404":{"description":"Snapshot not found"},"500":{"description":"Snapshot storage failed"}}}},"/curl.json":{"get":{"tags":["curl"],"summary":"HTTP fetch proxy","description":"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.","operationId":"handle_curl_json","parameters":[{"name":"url","in":"query","description":"URL to fetch","required":false,"schema":{"type":["string","null"]},"example":"https://example.com"}],"responses":{"200":{"description":"Successfully fetched URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CurlResponse"}}}},"400":{"description":"URL missing, malformed, or pointing at a disallowed target"},"404":{"description":"Failed to fetch URL (network error, etc.)"},"413":{"description":"Upstream body exceeded the 5 MiB cap"},"502":{"description":"DNS resolution for the target failed"}}}},"/echo.json":{"get":{"tags":["echo"],"summary":"Echo request details with fraud detection","description":"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).","operationId":"handle_echo_json","responses":{"200":{"description":"Echo response with request details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Echo"}}}},"500":{"description":"Internal server error"}}}},"/encoding.json":{"get":{"tags":["encoding"],"summary":"Encode and hash text values","description":"Computes various encodings (Base64, URL) and hashes (MD5, SHA1, SHA256) for the given input value. Also provides octal, decimal, and hex byte representations.","operationId":"handle_encoding_json","parameters":[{"name":"value","in":"query","description":"Raw input value to encode/decode.","required":false,"schema":{"type":"string"},"example":"Hello World"},{"name":"charset","in":"query","description":"Charset name, e.g. `UTF-8`, `ISO-8859-1`, etc.\n\nDefaults to `UTF-8` (matching the Kotlin version's `Charsets.UTF_8.name()`).","required":false,"schema":{"type":"string"},"example":"UTF-8"}],"responses":{"200":{"description":"Encoding results","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EncodingResponse"}}}},"500":{"description":"Internal server error"}}}},"/entropy.json":{"get":{"tags":["entropy"],"summary":"Generate random entropy values","description":"Returns a collection of random values including UUID, integers, floats, and a Gaussian-distributed number. Uses a fast (non-cryptographic) RNG.","operationId":"handle_entropy_json","responses":{"200":{"description":"Entropy values generated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntropyResponse"}}}},"500":{"description":"Internal server error"}}}},"/inspect.json":{"get":{"tags":["inspect"],"summary":"Request inspection combined with IP geolocation and ASN","description":"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.","operationId":"handle_inspect_json","parameters":[{"name":"queryIP","in":"query","description":"Override IP address for geolocation lookup","required":false,"schema":{"type":"string"},"example":"1.1.1.1"},{"name":"clientId","in":"query","description":"Optional client-supplied request ID for log correlation. NOT auth.","required":false,"schema":{"type":"string"},"example":"550e8400-e29b-41d4-a716-446655440000"},{"name":"response-header","in":"query","description":"Debug: echo `Name: Value` back as a response header. Restricted to a safe allowlist.","required":false,"schema":{"type":"string"},"example":"Cache-Control: no-store"}],"responses":{"200":{"description":"Inspection data combining request echo and IP geolocation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Inspection"}}}},"429":{"description":"Rate limit or monthly quota exceeded"},"500":{"description":"Internal server error"}},"security":[{},{"api_key":[]}]}},"/pages/{id}":{"get":{"tags":["pages"],"summary":"GET /pages/{id} - Fetch a page by its ID.","description":"This endpoint allows direct page lookup without requiring site credentials.\nThe page ID is a SHA-256 hash of the siteId + URL.","operationId":"handle_fetch_by_id","parameters":[{"name":"id","in":"path","description":"Page ID (SHA-256 hash)","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Page found","headers":{"Cache-Control":{"schema":{"type":"string"},"description":"Prevents representation transforms that would invalidate the ETag"},"ETag":{"schema":{"type":"string"},"description":"Strong validator for conditional replacement"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FetchedPage"}}}},"404":{"description":"Page not found"}}}},"/sites":{"post":{"tags":["sites"],"summary":"POST /sites - Create a new site","operationId":"handle_create_site","requestBody":{"content":{"application/json":{"schema":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SiteProfileCreation"}]}}}},"responses":{"201":{"description":"Site created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteCreation"}}}},"500":{"description":"Internal server error"}}}},"/sites/crawl":{"post":{"tags":["crawler"],"summary":"Handler for POST /sites/crawl (admin multi-site crawl)","operationId":"handle_admin_crawl","parameters":[{"name":"allSitesCrawl","in":"query","description":"Force crawl all sites regardless of last crawl time","required":true,"schema":{"type":"boolean"}},{"name":"isThrottled","in":"query","description":"Apply rate limiting","required":true,"schema":{"type":"boolean"}},{"name":"clearIndex","in":"query","description":"Clear index before crawl","required":true,"schema":{"type":"boolean"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SitesCrawlStatus"}}},"required":true},"responses":{"200":{"description":"Multi-site crawl completed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SitesCrawlStatus"}}}},"401":{"description":"Unauthorized - invalid service secret"}},"security":[{"admin_secret":[]}]}},"/sites/crawl/status":{"get":{"tags":["crawler"],"summary":"Handler for GET /sites/crawl/status","operationId":"handle_get_crawl_status","responses":{"200":{"description":"Crawl status retrieved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SitesCrawlStatus"}}}},"401":{"description":"Unauthorized - invalid service secret"}},"security":[{"admin_secret":[]}]},"put":{"tags":["crawler"],"summary":"Handler for PUT /sites/crawl/status","operationId":"handle_update_crawl_status","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SitesCrawlStatus"}}},"required":true},"responses":{"200":{"description":"Crawl status updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SitesCrawlStatus"}}}},"401":{"description":"Unauthorized - invalid service secret"}},"security":[{"admin_secret":[]}]}},"/sites/flush":{"put":{"tags":["sites"],"summary":"PUT /sites/flush - commit the search index (make recent writes visible)","operationId":"handle_flush","responses":{"204":{"description":"Indices flushed"},"400":{"description":"Invalid credentials"},"503":{"description":"Flush failed"}},"security":[{"admin_secret":[]}]}},"/sites/rss":{"post":{"tags":["sites"],"summary":"POST /sites/rss - Create new site and index RSS feed","operationId":"handle_new_rss_feed","parameters":[{"name":"feedUrl","in":"query","description":"RSS feed URL","required":true,"schema":{"type":"string"}},{"name":"stripHtmlTags","in":"query","description":"Strip HTML tags from content","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"Feed indexed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteIndexSummary"}}}},"400":{"description":"Invalid feed URL or content"}}}},"/sites/{siteId}":{"get":{"tags":["sites"],"summary":"GET /sites/{siteId} - Fetch all document IDs for a site","operationId":"handle_fetch_all","parameters":[{"name":"siteId","in":"path","description":"Site ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Document IDs","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}},"404":{"description":"No documents found"}}},"delete":{"tags":["sites"],"summary":"DELETE /sites/{siteId} - Clear all pages for a site","operationId":"handle_clear_site","parameters":[{"name":"siteId","in":"path","description":"Site ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Site cleared"},"204":{"description":"No content to clear"}},"security":[{"site_secret":[]}]}},"/sites/{siteId}/autocomplete":{"get":{"tags":["sites"],"summary":"GET /sites/{siteId}/autocomplete - Autocomplete suggestions","operationId":"handle_autocomplete","parameters":[{"name":"siteId","in":"path","description":"Site ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"query","in":"query","description":"Partial search query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Autocomplete suggestions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Autocomplete"}}}},"400":{"description":"Invalid site ID"}}}},"/sites/{siteId}/crawl":{"post":{"tags":["crawler"],"summary":"Handler for POST /sites/{siteId}/crawl","operationId":"handle_crawl","parameters":[{"name":"siteId","in":"path","description":"Site ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"url","in":"query","description":"URL to crawl","required":true,"schema":{"type":"string"}},{"name":"email","in":"query","description":"Email for notifications","required":false,"schema":{"type":"string"}},{"name":"sitemapsOnly","in":"query","description":"Only crawl URLs from sitemap","required":true,"schema":{"type":"boolean"}},{"name":"allowUrlWithQuery","in":"query","description":"Allow URLs with query parameters","required":true,"schema":{"type":"boolean"}},{"name":"pageBodyCssSelector","in":"query","description":"CSS selector for page body extraction","required":true,"schema":{"type":"string"}},{"name":"maxPages","in":"query","description":"Cap pages crawled this run; clamped to the 500 throttled ceiling (only lowers it)","required":false,"schema":{"type":"integer","minimum":0}}],"responses":{"200":{"description":"Crawl completed successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlerJobResult"}}}},"404":{"description":"Site not found or invalid credentials"},"500":{"description":"Crawl failed"}},"security":[{"site_secret":[]}]}},"/sites/{siteId}/email/setup-info":{"post":{"tags":["email"],"summary":"POST /sites/{siteId}/email/setup-info — send setup information email.","description":"Bearer-gated by the per-site secret. The pre-Bearer global counter\nthat previously sat in front of this handler was removed: it\nincremented before credentials were checked, so any three failed\nrequests could disable the endpoint for everyone until process\nrestart. Authentication is the rate limit.\n\nOnce auth passes, a per-site cooldown ([`EMAIL_COOLDOWN`]) prevents\nthe endpoint from being turned into an outbound spam / mail-bomb\nvector for the site's owner mailbox — nobody legitimately hits \"send\nme my setup info\" more than once per minute. The cooldown is claimed\n*before* the send rather than after: a transport that fails slowly is\nexactly when a retrying client would otherwise hammer the mailbox.\n\nThe address is the one stored on the site profile, never one supplied\nin the request, so this endpoint cannot be aimed at a third party.\n`email` is optional in the Getting Started Gadget, so a profile\nwithout one is a normal outcome and answers 204, not an error.","operationId":"handle_send_setup_info","parameters":[{"name":"siteId","in":"path","description":"Site ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Email sent successfully"},"204":{"description":"No email address is registered for this site"},"404":{"description":"Site not found or invalid credentials"},"429":{"description":"Cooldown active — retry after Retry-After"},"503":{"description":"Email delivery is unavailable"}},"security":[{"site_secret":[]}]}},"/sites/{siteId}/har":{"get":{"tags":["sites"],"summary":"GET /sites/{siteId}/har - Export site pages as HAR 1.2","description":"When real HAR data exists (from crawls after HAR capture was enabled),\nreturns filtered real HTTP metadata. Falls back to synthetic HAR from\npage content when no real HAR data is available (backward compatibility).","operationId":"handle_har_export","parameters":[{"name":"siteId","in":"path","description":"Site ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"status","in":"query","description":"Filter by HTTP status code","required":false,"schema":{"type":"integer","format":"int32","minimum":0}},{"name":"since","in":"query","description":"ISO 8601: entries crawled at or after","required":false,"schema":{"type":"string"}},{"name":"before","in":"query","description":"ISO 8601: entries crawled before","required":false,"schema":{"type":"string"}},{"name":"url","in":"query","description":"URL prefix filter","required":false,"schema":{"type":"string"}},{"name":"minTime","in":"query","description":"Minimum response time in ms","required":false,"schema":{"type":"number","format":"double"}},{"name":"maxTime","in":"query","description":"Maximum response time in ms","required":false,"schema":{"type":"number","format":"double"}},{"name":"contentType","in":"query","description":"MIME type filter","required":false,"schema":{"type":"string"}},{"name":"sort","in":"query","description":"Sort: time, status, crawledAt; prefix - for desc","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","description":"Max entries (default 100, max 10000)","required":false,"schema":{"type":"integer","minimum":0}},{"name":"offset","in":"query","description":"Skip first N entries","required":false,"schema":{"type":"integer","minimum":0}}],"responses":{"200":{"description":"HAR 1.2 export of crawled pages","content":{"application/json":{}}},"403":{"description":"Invalid site secret"},"404":{"description":"No pages found for site"}},"security":[{"site_secret":[]},{"admin_secret":[]}]}},"/sites/{siteId}/pages":{"get":{"tags":["sites"],"summary":"GET /sites/{siteId}/pages - Fetch page by URL","operationId":"handle_get_page_by_url","parameters":[{"name":"siteId","in":"path","description":"Site ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"url","in":"query","description":"Page URL","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Page found","headers":{"Cache-Control":{"schema":{"type":"string"},"description":"Prevents representation transforms that would invalidate the ETag"},"ETag":{"schema":{"type":"string"},"description":"Strong validator for conditional replacement"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FetchedPage"}}}},"404":{"description":"Page not found"}}},"put":{"tags":["sites"],"summary":"PUT /sites/{siteId}/pages - Add/update page in site index","operationId":"handle_put_page","parameters":[{"name":"siteId","in":"path","description":"Site ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"If-Match","in":"header","description":"Optional ETag precondition. Omit for legacy unconditional replacement; use * to require an existing page.","required":false,"schema":{"type":["string","null"]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SitePageInput"}}},"required":true},"responses":{"200":{"description":"Page indexed","headers":{"ETag":{"schema":{"type":"string"},"description":"Strong validator for the stored page"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FetchedPage"}}}},"400":{"description":"Malformed If-Match header or missing URL"},"404":{"description":"Invalid credentials"},"412":{"description":"If-Match did not match the current page","headers":{"ETag":{"schema":{"type":"string"},"description":"Current validator when the page exists"}}}},"security":[{"site_secret":[]}]},"delete":{"tags":["sites"],"summary":"DELETE /sites/{siteId}/pages - Delete page by URL","operationId":"handle_delete_page_by_url","parameters":[{"name":"siteId","in":"path","description":"Site ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"url","in":"query","description":"Page URL","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Page deleted"},"404":{"description":"Page not found"}},"security":[{"site_secret":[]}]}},"/sites/{siteId}/pages/{pageId}":{"put":{"tags":["sites"],"summary":"PUT /sites/{siteId}/pages/{pageId} - Update existing page by ID","operationId":"handle_put_page_by_id","parameters":[{"name":"siteId","in":"path","description":"Site ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"pageId","in":"path","description":"Page ID (SHA-256 hash, 64 characters)","required":true,"schema":{"type":"string"}},{"name":"If-Match","in":"header","description":"Optional ETag precondition. Omit for legacy unconditional replacement; use * to require an existing page.","required":false,"schema":{"type":["string","null"]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SitePageInput"}}},"required":true},"responses":{"200":{"description":"Page updated","headers":{"ETag":{"schema":{"type":"string"},"description":"Strong validator for the stored page"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FetchedPage"}}}},"400":{"description":"Invalid page ID length or missing URL"},"404":{"description":"Invalid credentials or page not found"},"412":{"description":"If-Match did not match the current page","headers":{"ETag":{"schema":{"type":"string"},"description":"Current validator when the page exists"}}}},"security":[{"site_secret":[]}]},"delete":{"tags":["sites"],"summary":"DELETE /sites/{siteId}/pages/{pageId} - Delete page by ID","operationId":"handle_delete_page","parameters":[{"name":"siteId","in":"path","description":"Site ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"pageId","in":"path","description":"Page ID","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Page deleted"},"404":{"description":"Page not found"}},"security":[{"site_secret":[]}]}},"/sites/{siteId}/pages/{pageId}/backdate":{"put":{"tags":["sites"],"summary":"PUT /sites/{siteId}/pages/{pageId}/backdate?updated=<rfc3339>","description":"Admin-only test affordance — see [`SiteState::backdate_page`]. Overwrites a\npage's `updated` timestamp so the recrawl obsolete-cleanup can be tested\nagainst a live deployment. Gated by the service/admin secret (same Bearer\ntoken as `/flush`), not a per-site secret.","operationId":"handle_backdate_page","parameters":[{"name":"siteId","in":"path","description":"Site ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"pageId","in":"path","description":"Page ID","required":true,"schema":{"type":"string"}},{"name":"updated","in":"query","description":"New RFC 3339 `updated` timestamp","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Page timestamp updated"},"403":{"description":"Invalid admin credentials"},"404":{"description":"Page not found"}},"security":[{"admin_secret":[]}]}},"/sites/{siteId}/profile":{"get":{"tags":["sites"],"summary":"GET /sites/{siteId}/profile - Fetch site profile","operationId":"handle_get_profile","parameters":[{"name":"siteId","in":"path","description":"Site ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Profile found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteProfile"}}}},"404":{"description":"Profile not found"}},"security":[{"site_secret":[]},{"admin_secret":[]}]},"put":{"tags":["sites"],"summary":"PUT /sites/{siteId}/profile - Update site profile","operationId":"handle_update_profile","parameters":[{"name":"siteId","in":"path","description":"Site ID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteProfileUpdate"}}},"required":true},"responses":{"200":{"description":"Profile updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteProfile"}}}},"404":{"description":"Profile not found"}},"security":[{"site_secret":[]}]}},"/sites/{siteId}/recrawl":{"post":{"tags":["crawler"],"summary":"Handler for POST /sites/{siteId}/recrawl","operationId":"handle_recrawl","parameters":[{"name":"siteId","in":"path","description":"Site ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"clearIndex","in":"query","description":"Clear existing index before recrawl","required":true,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"Recrawl completed successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlerJobResult"}}}},"404":{"description":"Site not found or invalid credentials"},"500":{"description":"Recrawl failed"}},"security":[{"site_secret":[]}]}},"/sites/{siteId}/rss":{"put":{"tags":["sites"],"summary":"PUT /sites/{siteId}/rss - Index RSS feed into existing site","operationId":"handle_rss_feed","parameters":[{"name":"siteId","in":"path","description":"Site ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"feedUrl","in":"query","description":"RSS feed URL","required":true,"schema":{"type":"string"}},{"name":"stripHtmlTags","in":"query","description":"Strip HTML tags from content","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"Feed indexed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteIndexSummary"}}}},"400":{"description":"Invalid feed URL or content"},"404":{"description":"Site not found or invalid credentials"}},"security":[{"site_secret":[]}]}},"/sites/{siteId}/search":{"get":{"tags":["sites"],"summary":"GET /sites/{siteId}/search - Search within a site","operationId":"handle_search","parameters":[{"name":"siteId","in":"path","description":"Site ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"query","in":"query","description":"Search query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Search results","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResult"}}}},"400":{"description":"Invalid site ID"}}}},"/sites/{siteId}/xml":{"put":{"tags":["sites"],"summary":"PUT /sites/{siteId}/xml - Index generic XML feed into existing site","operationId":"handle_xml_import","parameters":[{"name":"siteId","in":"path","description":"Site ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"xmlUrl","in":"query","description":"XML feed URL","required":true,"schema":{"type":"string"}},{"name":"stripHtmlTags","in":"query","description":"Strip HTML tags from content","required":false,"schema":{"type":"boolean"}},{"name":"clearIndex","in":"query","description":"Clear existing index before importing","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"XML indexed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteIndexSummary"}}}},"400":{"description":"Invalid XML URL or content"},"404":{"description":"Site not found or invalid credentials"}},"security":[{"site_secret":[]}]}},"/stats.json":{"get":{"tags":["stats"],"summary":"Build + health info","description":"Returns api status, version, build hash, build number, and uptime. No per-client data. For per-key usage, see the authenticated metering APIs.","operationId":"handle_stats","responses":{"200":{"description":"Build and health info","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Stats"}}}}}}},"/test":{"get":{"tags":["test"],"summary":"Delayed response test endpoint","description":"Returns a response after an optional delay. Useful for testing timeouts, latency handling, and async behavior. Accepts any HTTP method.","operationId":"handle_test","parameters":[{"name":"timeoutInMs","in":"query","description":"Delay in milliseconds before responding (defaults to 0)","required":false,"schema":{"type":["integer","null"],"format":"int64","minimum":0},"example":100}],"responses":{"200":{"description":"Triggered response after delay","content":{"text/plain":{"schema":{"type":"string"},"example":"Triggered after 100 ms."}}}}}},"/visit.json":{"post":{"tags":["visit"],"summary":"Visit beacon with bot / AI-agent recognition","description":"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.","operationId":"handle_visit_json","parameters":[{"name":"queryIP","in":"query","description":"Override IP for the geolocation / ASN lookup","required":false,"schema":{"type":"string"},"example":"1.1.1.1"}],"requestBody":{"description":"Client runtime signals from the tracking tag (all optional)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientSignals"}}},"required":true},"responses":{"200":{"description":"Visit verdict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Visit"}}}},"429":{"description":"Rate limit or monthly quota exceeded"},"500":{"description":"Internal server error"}},"security":[{},{"api_key":[]}]}},"/visit/engagement.json":{"post":{"tags":["visit"],"summary":"Attach engagement depth to a /analytics.js visit","description":"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.","operationId":"handle_visit_engagement","parameters":[{"name":"id","in":"query","description":"visit_id issued by GET /analytics.js","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Engagement-depth sample","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PageEngagement"}}},"required":true},"responses":{"200":{"description":"Accepted (fire-and-forget)"}},"security":[{},{"api_key":[]}]}},"/visit/enrich.json":{"post":{"tags":["visit"],"summary":"Enrich a /analytics.js visit with client signals","description":"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.","operationId":"handle_visit_enrich","parameters":[{"name":"id","in":"query","description":"visit_id issued by GET /analytics.js","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Client runtime signals","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientSignals"}}},"required":true},"responses":{"200":{"description":"Verdict (verdict / score / reasons) as JSON"}},"security":[{},{"api_key":[]}]}},"/visit/outbound.json":{"post":{"tags":["visit"],"summary":"Attach outbound clicks & downloads to a /analytics.js visit","description":"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.","operationId":"handle_visit_outbound","parameters":[{"name":"id","in":"query","description":"visit_id issued by GET /analytics.js","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Batched outbound events","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PageOutbound"}}},"required":true},"responses":{"200":{"description":"Accepted (fire-and-forget)"}},"security":[{},{"api_key":[]}]}},"/visit/retention.json":{"post":{"tags":["visit"],"summary":"Set a site's data-retention TTL (days) — requires siteSecret","operationId":"handle_set_retention","parameters":[{"name":"siteId","in":"query","description":"Site id (UUID) to configure","required":true,"schema":{"type":"string"}},{"name":"siteSecret","in":"query","description":"The site's write/config secret (UUID)","required":true,"schema":{"type":"string"}},{"name":"days","in":"query","description":"Retention in days (>= 1); default 365","required":true,"schema":{"type":"integer","format":"int64"},"example":365}],"responses":{"200":{"description":"Updated retention"},"400":{"description":"Missing/invalid siteId or days"},"401":{"description":"Missing or invalid siteSecret"},"503":{"description":"Metering backend temporarily unavailable — retry later (Retry-After)"}},"security":[{"site_secret_query":[]}]}},"/visit/stats.json":{"get":{"tags":["visit"],"summary":"Aggregated visit analytics for a site","description":"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; a non-UUID `siteId` is rejected.","operationId":"handle_visit_stats","parameters":[{"name":"siteId","in":"query","description":"Site id (UUID) whose analytics to read","required":true,"schema":{"type":"string"}},{"name":"days","in":"query","description":"Window length in days (default 7, max 365)","required":false,"schema":{"type":"integer","format":"int32","minimum":0},"example":7}],"responses":{"200":{"description":"Aggregated visit analytics","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VisitStats"}}}},"400":{"description":"Missing or non-UUID `siteId` parameter"},"500":{"description":"Internal server error"},"503":{"description":"Metering backend unavailable or report capacity busy - retry later (Retry-After: 30)"}},"security":[{},{"api_key":[]}]}},"/visit/visibility.json":{"post":{"tags":["visit"],"summary":"Toggle a site's analytics public/private — requires siteSecret","operationId":"handle_set_visibility","parameters":[{"name":"siteId","in":"query","description":"Site id (UUID) to configure","required":true,"schema":{"type":"string"}},{"name":"siteSecret","in":"query","description":"The site's write/config secret (UUID)","required":true,"schema":{"type":"string"}},{"name":"private","in":"query","description":"true = private (stats reads need the siteSecret); false = public","required":true,"schema":{"type":"boolean"},"example":true}],"responses":{"200":{"description":"Updated visibility"},"400":{"description":"Missing/invalid siteId or private flag"},"401":{"description":"Missing or invalid siteSecret"},"503":{"description":"Metering backend temporarily unavailable — retry later (Retry-After)"}},"security":[{"site_secret_query":[]}]}},"/visit/vitals.json":{"post":{"tags":["visit"],"summary":"Attach Core Web Vitals to a /analytics.js visit","description":"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.","operationId":"handle_visit_vitals","parameters":[{"name":"id","in":"query","description":"visit_id issued by GET /analytics.js","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Core Web Vitals sample","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PageVitals"}}},"required":true},"responses":{"200":{"description":"Accepted (fire-and-forget)"}},"security":[{},{"api_key":[]}]}},"/whois.json":{"get":{"tags":["whois"],"summary":"IP geolocation and ASN lookup","description":"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.","operationId":"handle_whois_json","parameters":[{"name":"queryIP","in":"query","description":"Override IP address for lookup","required":false,"schema":{"type":"string"},"example":"1.1.1.1"},{"name":"clientId","in":"query","description":"Optional client-supplied request ID for log correlation. NOT auth.","required":false,"schema":{"type":"string"},"example":"550e8400-e29b-41d4-a716-446655440000"}],"responses":{"200":{"description":"GeoIP + ASN data for the caller or the overridden IP","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Whois"}}}},"429":{"description":"Rate limit or monthly quota exceeded"},"500":{"description":"Internal server error"}},"security":[{},{"api_key":[]}]}}},"components":{"schemas":{"ActiveCrawlsResponse":{"type":"object","description":"Response shape for `GET /admin/active-crawls` (§7.4).","required":["running","site_ids"],"properties":{"running":{"type":"integer","minimum":0},"site_ids":{"type":"array","items":{"type":"string","format":"uuid"}}}},"AnalyticsStatsSummary":{"type":"object","description":"Last successful `visit_stats` compute, published on `/stats.json` as\n`analyticsStats` so the `redis-capacity` CI job can page when the compute\ntime drifts toward STATS_COMPUTE_CEILING - the growth signal that would\notherwise end in a permanent 503 (the 2026-09-02 lesson: a warning nobody\nreads is not detection).","required":["lastComputeMs","lastOkUnixMs","site","days"],"properties":{"days":{"type":"integer","format":"int64","description":"The window it computed, in days."},"lastComputeMs":{"type":"integer","format":"int64","description":"Wall-clock milliseconds of the most recent successful compute.","minimum":0},"lastOkUnixMs":{"type":"integer","format":"int64","description":"Unix milliseconds when that compute finished."},"site":{"type":"string","description":"The site it computed (the busiest site dominates in practice)."}}},"Asset":{"type":"object","description":"A published asset. `ownerId` is the manager's secret and is only returned to\nthe root manager/export surface, never to delegated or public views.","required":["id","ownerId","name","kind","currency","createdAt"],"properties":{"bookingFields":{"type":"array","items":{"$ref":"#/components/schemas/BookingFieldDef"},"description":"Owner-defined fields a consumer must/may fill when booking (empty = none)."},"createdAt":{"type":"integer","format":"int64","description":"Epoch-ms creation time.","minimum":0},"currency":{"type":"string","description":"Currency the window costs are quoted in — a free-form symbol or code\n(`$`, `USD`, `EUR`, `£`, `CHF`, …); **not necessarily USD**."},"description":{"type":["string","null"]},"id":{"type":"string","description":"Unguessable UUID; doubles as the shareable consumer link."},"kind":{"type":"string","description":"Free-form category, e.g. `drone`, `court`, `vehicle`, `lesson`, `property`."},"name":{"type":"string","description":"Human name, e.g. \"Court 1\", \"DJI Mavic 3\", \"Grand-piano lesson\"."},"ownerId":{"type":"string","description":"The managing owner's secret UUID."},"policy":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/BookingPolicy","description":"Optional booking/cancellation policy (`None` = book/cancel anytime)."}]},"promoCodes":{"type":"array","items":{"$ref":"#/components/schemas/PromoCode"},"description":"Owner-defined discount codes (empty = none). Only ever returned on the\nmanager surface — never leaked on the public [`AssetView`]."},"timezone":{"type":"string","description":"IANA timezone the window times are meant to be read in (e.g.\n`Europe/Berlin`); defaults to `UTC`. Times on the wire stay epoch-ms UTC —\nthis only drives how a client renders them."}}},"AssetRowsResponse":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/AssetWithWindows"}},{"type":"array","items":{"$ref":"#/components/schemas/DelegatedAssetWithWindows"}},{"type":"array","items":{"$ref":"#/components/schemas/ViewerAssetWithWindows"}}],"description":"Role-dependent serialization for `GET /owners/{token}/assets`."},"AssetView":{"type":"object","description":"The public consumer view of an asset: descriptive fields + windows, **without**\nthe `ownerId` and without other consumers' booking details.","required":["id","name","kind","currency","timezone","windows"],"properties":{"bookingFields":{"type":"array","items":{"$ref":"#/components/schemas/BookingFieldDef"},"description":"Custom fields the consumer must/may fill when booking (empty = none)."},"currency":{"type":"string"},"description":{"type":["string","null"]},"id":{"type":"string"},"kind":{"type":"string"},"name":{"type":"string"},"timezone":{"type":"string","description":"IANA timezone the consumer should read the slots in (default `UTC`)."},"windows":{"type":"array","items":{"$ref":"#/components/schemas/PublicWindow"}}}},"AssetWithWindows":{"type":"object","description":"Manager view of one asset: the asset plus all its windows (with bookings).\nIn the lightweight summary view (`?windows=false`) `windows` is empty and only\n`windowCount` is populated — so listing a large catalog doesn't inline every\nwindow+booking.","required":["asset"],"properties":{"asset":{"$ref":"#/components/schemas/Asset"},"windowCount":{"type":"integer","format":"int32","description":"Total windows on the asset (always set; the only window info in the summary view).","minimum":0},"windows":{"type":"array","items":{"$ref":"#/components/schemas/Window"}}}},"AuditAction":{"type":"object","description":"One remediation objective backed by the checks that contributed to it.","properties":{"category":{"type":"string","description":"Report area associated with the highest-severity contributor.","default":""},"contributingCheckIds":{"type":"array","items":{"type":"string"},"description":"Stable check or finding IDs merged into this action.","default":[]},"evidence":{"type":"array","items":{"type":"string"},"description":"Bounded observations supporting this action.","default":[]},"id":{"type":"string","description":"Stable remediation-family identifier, for example `header.csp`.","default":""},"recommendation":{"type":"string","description":"Concrete remediation guidance from the highest-severity contributor.","default":""},"severity":{"type":"string","description":"Highest advisory severity among the contributing checks.","default":"medium"},"title":{"type":"string","description":"Short action label.","default":""}}},"AuditCoverage":{"type":"object","description":"How much of the fixed one-page audit fan-out returned usable response\nevidence. Coverage is independent of both health scores: a complete report\ncan faithfully describe an unhealthy origin, while consumers can use a\npartial report to distinguish unevaluated checks from the frozen legacy\nscore and absence-shaped finding contract.","properties":{"auxiliaryResponseReceived":{"type":"boolean","description":"At least one same-origin auxiliary probe returned an HTTP response.\nTogether with `pageResponseReceived: false`, this fact identifies\ndifferential reachability without guessing whether the differentiator\nwas an edge policy, WAF, client fingerprint, IP, or something else.","default":false},"complete":{"type":"boolean","description":"Every scheduled probe returned an HTTP response and a complete body.","default":false},"completedProbes":{"type":"integer","description":"Probes that returned an HTTP response without truncation.","default":0,"minimum":0},"pageResponseReceived":{"type":"boolean","description":"The primary page returned an HTTP response, irrespective of its status.","default":false},"totalProbes":{"type":"integer","description":"Probes scheduled for this audit.","default":0,"minimum":0}}},"AuditEvent":{"type":"object","description":"One entry in an owner's activity log (audit trail). Append-only, newest-first,\ncapped at [`AUDIT_MAX`] and TTL'd like the rest of the owner's data.","required":["at","action"],"properties":{"action":{"type":"string","description":"Machine-readable action: `owner.create`, `asset.create`, `asset.update`,\n`asset.delete`, `window.add`, `window.update`, `window.delete`,\n`availability.generate`, `reserve`, `hold`, `confirm`, `cancel`, `modify`,\n`webhook.add`, `webhook.delete`, `member.add`, `member.delete`,\n`waitlist.join`, `waitlist.leave`, `waitlist.promote`, `owner.import`,\n`window.bulk_delete`."},"assetId":{"type":["string","null"]},"at":{"type":"integer","format":"int64","description":"Epoch-ms when the action happened.","minimum":0},"detail":{"type":["string","null"],"description":"Short human detail (asset name, booker handle, generated counts, …)."},"reservationId":{"type":["string","null"]},"windowId":{"type":["string","null"]}}},"AuditProvenance":{"type":"object","description":"Compact provenance for a completed one-page report.","properties":{"capturedAt":{"type":"integer","format":"int64","description":"Server capture time in Unix epoch milliseconds.","default":0,"minimum":0},"effectiveOrigin":{"type":["string","null"],"description":"Effective scheme/host/port, only when the page returned HTTP.","default":null},"effectiveUrl":{"type":["string","null"],"description":"Final URL observed after redirects, only when the page returned HTTP.","default":null},"engineVersion":{"type":"string","description":"Version of the Site Audit engine that produced the report.","default":""},"httpClientProfileId":{"type":"string","description":"Stable request/client profile used by the fetcher.","default":""},"pageEncoding":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PageEncodingProvenance","description":"Decoder selected for the primary HTML response."}],"default":null},"reportSchemaVersion":{"type":"integer","format":"int32","description":"Version of the top-level report wire schema.","default":0,"minimum":0},"sourceRevision":{"type":["string","null"],"description":"Exact deployed Git revision when a release supplied one.","default":null},"vantage":{"type":["string","null"],"description":"Coarse operator-defined capture vantage, never an egress IP or hostname.","default":null}}},"AuditReport":{"type":"object","description":"Top-level audit report.","properties":{"actions":{"type":"array","items":{"$ref":"#/components/schemas/AuditAction"},"description":"Deterministic remediation objectives derived from existing findings and\nfailed checks. Additive and independent of every score.","default":[]},"cdn":{"oneOf":[{"$ref":"#/components/schemas/CdnInfo","description":"CDN / edge-network detection for the origin (see [`CdnInfo`])."}],"default":{"detected":false}},"coverage":{"oneOf":[{"$ref":"#/components/schemas/AuditCoverage","description":"Response coverage for the fixed one-page probe set."}],"default":{"complete":false,"pageResponseReceived":false,"auxiliaryResponseReceived":false,"completedProbes":0,"totalProbes":0}},"findingDetails":{"type":"array","items":{"$ref":"#/components/schemas/FindingDetail"},"description":"The same findings, in the same order, each carrying a stable catalogue\nid, a severity, and the what/why/how explanation needed to act on it\n(see [`explain`]). Additive: `findings` remains the established field,\nand a snapshot captured before this existed simply deserializes empty.","default":[]},"findings":{"type":"array","items":{"type":"string"},"description":"Notable issues found, human-readable.","default":[]},"health":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/HealthReport","description":"Additive evidence-aware health result. The legacy `score` and its gates,\ndiffs, and watch semantics remain unchanged for compatibility."}],"default":null},"host":{"type":"string","description":"The normalized host that was audited.","default":""},"meta":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PageMeta","description":"SEO / correctness facts extracted from the page HTML (see [`PageMeta`])."}],"default":null},"probes":{"type":"array","items":{"$ref":"#/components/schemas/ProbeResult"},"description":"Per-URL results, page first then well-known paths sorted.","default":[]},"provenance":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/AuditProvenance","description":"Capture, effective-origin, and page-decoding evidence for new reports."}],"default":null},"score":{"type":"integer","format":"int32","description":"Legacy HTTP/security health score 0–100. Its established weighting and\ngate semantics are intentionally independent of the versioned SEO score.","default":0,"minimum":0},"security":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SecurityAnalysis","description":"Security-header **quality** analysis for the page (see [`SecurityAnalysis`])."}],"default":null},"seo":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SeoReport","description":"Separately versioned technical search-readiness analysis. This never\nchanges the legacy `score`, gate/diff semantics, or watch thresholds;\nSEO gates and SEO diffs are explicitly opt-in."}],"default":null},"shareId":{"type":["string","null"],"description":"Set when this report was published as a shareable copy (`?share=true`):\nthe unguessable id it is retrievable under at\n`/audit/snapshots/{shareId}`. Absent means the report was not stored,\nso nothing about this run is retrievable by anyone else.","default":null},"target":{"type":"string","description":"The audited URL, normalized and with any `user:pass@` credentials\nstripped (never the raw input — see [`run_audit`]).","default":""},"tls":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/TlsInfo","description":"TLS leaf-certificate facts for an HTTPS origin (expiry, issuer). Absent\nfor `http://` or when the handshake failed (see [`crate::audit::tls`])."}],"default":null},"unicodeHost":{"type":["string","null"],"description":"Unicode presentation form of an IDN host. `host` remains the canonical\nASCII/Punycode security and network identity.","default":null}}},"AuditRowsResponse":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/AuditEvent"}},{"type":"array","items":{"$ref":"#/components/schemas/DelegatedAuditEvent"}}],"description":"Role-dependent serialization for the manager audit trail."},"Autocomplete":{"type":"object","description":"Autocomplete response.\nEquivalent to Kotlin `Autocomplete`.","required":["results"],"properties":{"results":{"type":"array","items":{"type":"string"}}}},"AvailabilityResult":{"type":"object","description":"Result of a bulk `availability` generation.","required":["created","skipped"],"properties":{"created":{"type":"integer","format":"int32","description":"Windows actually created.","minimum":0},"skipped":{"type":"integer","format":"int32","description":"Candidate windows skipped (overlapped an existing window, or hit the cap).","minimum":0}}},"BookingCounts":{"type":"object","description":"Booking counts by (effective) lifecycle state for a [`OwnerReport`].","required":["confirmed","held","cancelled","completed","expired"],"properties":{"cancelled":{"type":"integer","format":"int32","minimum":0},"completed":{"type":"integer","format":"int32","minimum":0},"confirmed":{"type":"integer","format":"int32","minimum":0},"expired":{"type":"integer","format":"int32","minimum":0},"held":{"type":"integer","format":"int32","minimum":0}}},"BookingFieldDef":{"type":"object","description":"An owner-defined field a consumer fills in at booking time — e.g. a license\nplate, headcount, delivery address, or a `select` from a fixed option list.\nDefinitions live on the [`Asset`]; the consumer-supplied values live on the\n[`Reservation`] (`fields`). Cross-vertical and self-contained.","required":["key","label"],"properties":{"key":{"type":"string","description":"Stable machine key (`[A-Za-z0-9_-]`, unique per asset) — the key under which\nthe consumer's value is stored in `Reservation.fields`."},"label":{"type":"string","description":"Human-facing label the booking UI renders."},"options":{"type":"array","items":{"type":"string"},"description":"Allowed values for a `select` field (ignored for other types)."},"required":{"type":"boolean","description":"Whether the consumer must supply a non-empty value."},"type":{"$ref":"#/components/schemas/FieldType","description":"Value shape (default `text`)."}}},"BookingPolicy":{"type":"object","description":"A per-asset booking policy: how close to a slot's start a consumer may still\nbook it, and how close they may still cancel it. `0` = no restriction; an\nall-zero policy is normalised to \"no policy\". Owner-initiated cancels always\nbypass the cancel cutoff.","properties":{"cancelCutoffMinutes":{"type":"integer","format":"int32","description":"A consumer can't cancel a booking within this many minutes of the slot start.","minimum":0},"minNoticeMinutes":{"type":"integer","format":"int32","description":"A consumer can't book/hold a window starting within this many minutes of now.","minimum":0}}},"BrokenLink":{"type":"object","description":"One broken internal link found during the crawl.","required":["url","referencedBy"],"properties":{"referencedBy":{"type":"string","description":"A page that referenced this link (the first one seen)."},"status":{"type":["integer","null"],"format":"int32","description":"The HTTP status (4xx/5xx), or `None` when the link was unreachable.","minimum":0},"url":{"type":"string"}}},"BulkDeleteResult":{"type":"object","description":"Result of a bulk window delete.","required":["deleted"],"properties":{"deleted":{"type":"integer","format":"int32","minimum":0}}},"CdnChange":{"type":"object","description":"A report-level CDN / edge-network transition between two snapshots. This is\nan *origin* property (every same-origin probe sees the same CDN), so it lives\nbeside `score_delta` rather than in the per-URL `changes` list.","required":["kind"],"properties":{"after":{"type":["string","null"],"description":"Vendor on the AFTER snapshot."},"before":{"type":["string","null"],"description":"Vendor on the BEFORE snapshot (absent when unnamed or not detected)."},"kind":{"type":"string","description":"`added` (origin → now behind a CDN), `removed` (CDN no longer\nfingerprinted), or `changed` (different vendor, e.g. Fastly → Cloudflare)."}}},"CdnInfo":{"type":"object","description":"Whether — and which — CDN / edge network fronts the audited origin, inferred\nfrom response headers. Derived at fetch time and kept deliberately compact\nand *value-free*: the tell-tale headers (`cf-ray`, `x-amz-cf-id`, …) carry\nper-request-unique values, so we record only the header **names** that\nmatched. That keeps the signal stable across re-audits (no spurious\n`header-changed` in the snapshot diff).","properties":{"detected":{"type":"boolean","description":"True when a CDN/edge network was positively identified (a vendor\nsignature matched, or the RFC 8586 `CDN-Loop` header was present).","default":false},"evidence":{"type":"array","items":{"type":"string"},"description":"Stable evidence: the response-header names that triggered the match\n(values omitted — see the struct docs).","default":[]},"vendor":{"type":["string","null"],"description":"The identified provider (e.g. `Cloudflare`, `Fastly`, `Amazon\nCloudFront`) when a vendor-specific signature matched; `None` when a CDN\nis present but the vendor could not be named.","default":null}}},"ChainCert":{"type":"object","description":"One certificate in the chain the server presented (leaf and each additional\ncert), summarised for the report. A compact, SSL-Labs-style chain view.","properties":{"issuer":{"type":["string","null"],"default":null},"keyBits":{"type":["integer","null"],"format":"int32","default":null,"minimum":0},"keyType":{"type":["string","null"],"description":"`EC` / `RSA` / `Ed25519`.","default":null},"notAfter":{"type":"integer","format":"int64","description":"`notAfter`, epoch seconds.","default":0},"signatureAlgorithm":{"type":["string","null"],"default":null},"subject":{"type":["string","null"],"default":null}}},"ClientSignals":{"type":"object","description":"Client-side runtime signals POSTed by the tag. Every field is optional:\na *missing* field is \"unknown\" and is **not** penalised, only a field that\nis present-and-bot-like contributes to the score (so an empty `{}` beacon\nscores 0 on the client side and falls back to the server-side verdict).","properties":{"hardwareConcurrency":{"type":["number","null"],"format":"double","description":"`navigator.hardwareConcurrency` — 0 is a headless tell.","default":null},"languagesCount":{"type":["integer","null"],"format":"int32","description":"`navigator.languages.length` — headless contexts often report 0.","default":null,"minimum":0},"pluginsCount":{"type":["integer","null"],"format":"int32","description":"`navigator.plugins.length` — 0 on a non-mobile UA is mildly suspect.","default":null,"minimum":0},"referrer":{"type":["string","null"],"description":"`document.referrer` — the visit's traffic source. Reduced to its host\nand stored for the top-referrers rollup; never used for scoring.","default":null},"url":{"type":["string","null"],"description":"`location.href` of the tracked page — used only to drop same-site\n(internal-navigation) referrers from that rollup.","default":null},"webdriver":{"type":["boolean","null"],"description":"`navigator.webdriver` — true under automation (Selenium/Puppeteer/…).","default":null}}},"CountRow":{"type":"object","description":"One row of a \"top N\" breakdown — a path (top pages) or a referrer host\n(top referrers) with its visitor / view split over the window.","required":["label","uniqueVisitors","visits","human","bot"],"properties":{"bot":{"type":"integer","format":"int64"},"human":{"type":"integer","format":"int64"},"label":{"type":"string","description":"The path (top pages) or referrer host (top referrers)."},"uniqueVisitors":{"type":"integer","format":"int64","description":"Distinct visitors (by client IP) — the sort key for the breakdown."},"visits":{"type":"integer","format":"int64","description":"Page views (total beacons) for this label."}}},"CrawlCoverage":{"type":"object","description":"Honest accounting for the bounded discovery and link sample.","properties":{"homepageLinksDiscovered":{"type":"integer","default":0,"minimum":0},"homepageLinksTruncated":{"type":"boolean","default":false},"incompletePages":{"type":"integer","default":0,"minimum":0},"internalLinksChecked":{"type":"integer","default":0,"minimum":0},"internalLinksDiscovered":{"type":"integer","default":0,"minimum":0},"linkCheckLimitReached":{"type":"boolean","default":false},"pageLimitReached":{"type":"boolean","default":false},"pagesAudited":{"type":"integer","default":0,"minimum":0},"pagesDiscovered":{"type":"integer","default":0,"minimum":0},"pagesSelected":{"type":"integer","default":0,"minimum":0},"pagesWithTruncatedLinks":{"type":"integer","default":0,"minimum":0},"redirectBudgetExhausted":{"type":"boolean","description":"At least one otherwise-safe same-host redirect was not followed because\nits additional request could not be admitted under the target budget.","default":false},"requestedLimit":{"type":"integer","default":0,"minimum":0},"siteSearchTruncated":{"type":"boolean","default":false},"siteSearchUrlsDiscovered":{"type":"integer","default":0,"minimum":0},"sitemapByteLimitReached":{"type":"boolean","default":false},"sitemapBytesRead":{"type":"integer","description":"Raw response bytes retained across sitemap attempts, including error\nresponses; this is resource accounting, not valid sitemap XML size.","default":0,"minimum":0},"sitemapDocumentReferencesRejected":{"type":"integer","description":"Invalid, out-of-scope, too-deep, or over-queue-limit document references\nrejected before admission. These are not discovered/skipped documents.","default":0,"minimum":0},"sitemapDocumentsAttempted":{"type":"integer","description":"Admitted document candidates for which an HTTP request was attempted.","default":0,"minimum":0},"sitemapDocumentsByteLimited":{"type":"integer","default":0,"minimum":0},"sitemapDocumentsDiscovered":{"type":"integer","description":"Distinct normalized, in-scope sitemap document candidates admitted to\nthe traversal queue, including the two conventional optional probes.","default":0,"minimum":0},"sitemapDocumentsFailed":{"type":"integer","description":"Transport failures and every non-2xx status except a 404 from an optional\nconventional probe. This can overlap `fetched` for HTTP failures.","default":0,"minimum":0},"sitemapDocumentsFetched":{"type":"integer","description":"Attempts that received an HTTP response, regardless of its status.","default":0,"minimum":0},"sitemapDocumentsInvalid":{"type":"integer","description":"Fully received responses whose XML/content is malformed or unsupported.","default":0,"minimum":0},"sitemapDocumentsSkipped":{"type":"integer","description":"Admitted document candidates left unattempted when a traversal cap was\nreached. Together with `attempted`, this partitions `discovered`.","default":0,"minimum":0},"sitemapDocumentsStreamInterrupted":{"type":"integer","default":0,"minimum":0},"sitemapDocumentsTruncated":{"type":"integer","description":"Responses not read to EOF, partitioned by the next two counters.","default":0,"minimum":0},"sitemapEntriesSeen":{"type":"integer","default":0,"minimum":0},"sitemapEntryLimitReached":{"type":"boolean","default":false},"sitemapExists":{"type":"boolean","default":false},"sitemapFetchLimitReached":{"type":"boolean","default":false},"sitemapLastmodFuture":{"type":"integer","default":0,"minimum":0},"sitemapLastmodInvalid":{"type":"integer","default":0,"minimum":0},"sitemapQueueLimitReached":{"type":"boolean","default":false},"sitemapUrlsDuplicate":{"type":"integer","default":0,"minimum":0},"sitemapUrlsInvalid":{"type":"integer","default":0,"minimum":0},"sitemapUrlsOutOfScope":{"type":"integer","default":0,"minimum":0},"sitemapUrlsValid":{"type":"integer","default":0,"minimum":0}}},"CrawlDiagnostic":{"type":"object","description":"Stable, sampled crawl-level diagnostic. Evidence is sorted, deduplicated,\nand capped so a link farm or malformed sitemap cannot inflate the report.","properties":{"evidence":{"type":"array","items":{"type":"string"},"default":[]},"id":{"type":"string","default":""},"message":{"type":"string","default":""},"sampled":{"type":"boolean","default":false},"severity":{"type":"string","default":""}}},"CrawlDiagnostics":{"type":"object","description":"Why a crawl indexed the page count it did.\n\nReturned on [`CrawlerJobResult::diagnostics`]. Every counter is the\nnumber of URLs that ended in that outcome; `indexed` equals\n[`CrawlerJobResult::page_count`]. `summary` is a human-readable\nexplanation, most useful when `indexed` is zero or every indexed page\nhad no extractable text (a client-side-rendered site).","required":["fetched","indexed","emptyBody","httpError","nonHtml","fetchError","robotsBlocked","indexError","summary","samples"],"properties":{"depthFiltered":{"type":"integer","format":"int64","description":"URLs excluded because they sit deeper than the crawl's max depth\n(link-distance from the seed). The depth cap is an internal crawl\nbound, so a large count means the site's structure is deeper than\nthe crawler follows — same invisible-drop family as\n`scope_filtered`. Additive field; `#[serde(default)]` keeps\npre-field payloads parseable.","minimum":0},"emptyBody":{"type":"integer","format":"int64","description":"Subset of `indexed` whose extracted body text was empty — the\nsignal of a client-side-rendered (JavaScript SPA) page that will\nnot produce useful search results.","minimum":0},"fetchError":{"type":"integer","format":"int64","description":"URLs that never produced a usable HTTP response (DNS, TLS,\nconnection, timeout, or an empty/unreadable body).","minimum":0},"fetched":{"type":"integer","format":"int64","description":"URLs that reached a fetch attempt and produced a terminal outcome\n(everything below except `robotsBlocked` and `scopeFiltered`, which\nare excluded before any HTTP request is made).","minimum":0},"frontierDropped":{"type":"integer","format":"int64","description":"Candidate URLs shed because the crawl frontier hit its internal\nbound (`FRONTIER_MAX`) — only plausible on extremely link-dense\nhosts. Sheds are candidates that were never evaluated, not pages\nthat failed. Additive field; `#[serde(default)]` keeps pre-field\npayloads parseable.","minimum":0},"httpError":{"type":"integer","format":"int64","description":"Fetched URLs that returned a non-2xx HTTP status (often 403 bot\nprotection or a 5xx).","minimum":0},"indexError":{"type":"integer","format":"int64","description":"Pages fetched successfully but rejected by the search backend on\nwrite — a server-side error, not a problem with the crawled site.\n`indexed` (and `pageCount`) already exclude these, so the response\nstays truthful about what the store actually holds.","minimum":0},"indexed":{"type":"integer","format":"int64","description":"Pages stored in the index. Equals `CrawlerJobResult.pageCount`.","minimum":0},"nonHtml":{"type":"integer","format":"int64","description":"2xx responses whose content-type was not indexable HTML/PDF.","minimum":0},"robotsBlocked":{"type":"integer","format":"int64","description":"URLs excluded before fetch because robots.txt disallows the\ncrawler user-agent (`opty`).","minimum":0},"samples":{"type":"array","items":{"$ref":"#/components/schemas/DiagnosticSample"}},"scopeFiltered":{"type":"integer","format":"int64","description":"URLs excluded before fetch because they fall outside the crawl\nscope: the crawler only follows URLs under the SEED URL'S PATH\nPREFIX (`should_crawl_url`), plus extension/query filters. A large\ncount next to a tiny `indexed` is the signature of a too-narrow\nseed — e.g. an article URL instead of the host root (build #2391,\nwhere this drop was invisible and cost a live bisect to find).\n`#[serde(default)]` keeps payloads from pre-field builds parseable.","minimum":0},"seedRedirectedTo":{"type":["string","null"],"description":"Set when the seed URL redirected and the crawl rebased its scope\nonto the redirect's final scheme/host/port (`engine::rebase_scope`)\n— the operator gave one URL and the crawl ran against another, which\nqualification needs to know. Additive field; `#[serde(default)]`\nkeeps pre-field payloads parseable and `None` stays off the wire."},"summary":{"type":"string"}}},"CrawlPage":{"type":"object","description":"One audited page in a crawl.","required":["url","class","linkCount"],"properties":{"class":{"type":"string","description":"`present` / `redirected` / `error` / `unreachable`."},"complete":{"type":"boolean","description":"Whether the response body reached EOF without hitting the byte cap or a\nstream error. `false` for unreachable pages and legacy deserializations."},"contentType":{"type":["string","null"]},"finalUrl":{"type":["string","null"]},"linkCount":{"type":"integer","description":"Same-host links harvested from this page (for the link check).","minimum":0},"linksTruncated":{"type":"boolean","description":"True when more same-host links existed than the per-page crawl cap."},"pageEncoding":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PageEncodingProvenance","description":"Decoder selected for this page's bounded response body."}]},"provenance":{"type":"array","items":{"$ref":"#/components/schemas/DiscoverySource"},"description":"Every bounded discovery source which named this URL."},"seo":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SeoReport","description":"Per-page technical search-readiness analysis. Absent on unreachable pages\nand when reading an older serialized crawl."}]},"size":{"type":["integer","null"],"minimum":0},"status":{"type":["integer","null"],"format":"int32","minimum":0},"title":{"type":["string","null"]},"url":{"type":"string"}}},"CrawlReport":{"type":"object","description":"Full crawl report.","required":["target","host","discoveredVia","pagesDiscovered","pagesAudited","score","pages"],"properties":{"brokenLinks":{"type":"array","items":{"$ref":"#/components/schemas/BrokenLink"}},"complete":{"type":"boolean","description":"True when every operation in the selected bounded sample completed and\nno discovery/link resource limit was reached. This never means whole-web\ncoverage; [`sampled`] remains true."},"coverage":{"$ref":"#/components/schemas/CrawlCoverage"},"diagnostics":{"type":"array","items":{"$ref":"#/components/schemas/CrawlDiagnostic"}},"discoveredVia":{"type":"string","description":"How the page set was discovered: `sitemap` / `sitemap-index` /\n`homepage-links` / `site-search`."},"findings":{"type":"array","items":{"type":"string"}},"host":{"type":"string"},"pages":{"type":"array","items":{"$ref":"#/components/schemas/CrawlPage"}},"pagesAudited":{"type":"integer","minimum":0},"pagesDiscovered":{"type":"integer","description":"Distinct pages discovered before the `limit` cap.","minimum":0},"sampled":{"type":"boolean"},"score":{"type":"integer","format":"int32","description":"Aggregate health: percent of audited pages returning a healthy 2xx/3xx.","minimum":0},"seo":{"$ref":"#/components/schemas/CrawlSeoSummary"},"target":{"type":"string"},"unicodeHost":{"type":["string","null"],"description":"Unicode presentation form of an IDN host. `host` remains canonical\nASCII/Punycode for network and security decisions."}}},"CrawlSeoSummary":{"type":"object","description":"Mean SEO score over scoreable sampled pages, kept separate from the legacy\ncrawl health score.","properties":{"complete":{"type":"boolean","default":false},"meanScore":{"type":["integer","null"],"format":"int32","default":null,"minimum":0},"pagesComplete":{"type":"integer","default":0,"minimum":0},"pagesScored":{"type":"integer","default":0,"minimum":0},"pagesWithAnalysis":{"type":"integer","default":0,"minimum":0},"scoreModelVersion":{"type":["integer","null"],"format":"int32","default":null,"minimum":0}}},"CrawlStatus":{"type":"object","description":"Crawl status for a site.\nEquivalent to Kotlin `CrawlStatus`.","required":["crawled"],"properties":{"crawled":{"type":"string"},"pageCount":{"type":"integer","format":"int64"},"siteId":{"type":["string","null"],"format":"uuid"},"siteProfile":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SiteProfile"}]}}},"CrawlerJobResult":{"type":"object","description":"Result of a crawl job.\nEquivalent to Kotlin `CrawlerJobResult`.","required":["pageCount","urls"],"properties":{"diagnostics":{"$ref":"#/components/schemas/CrawlDiagnostics","description":"Why the crawl indexed the pages it did. Surfaced so a managed\nSite Search operator can answer the *qualification* question —\n\"can we usefully crawl this site?\" — from the crawl response\nalone, instead of from server logs (e.g. \"0 pages: robots.txt\nblocks us\", or \"indexed but the site is a JavaScript SPA\").\nAdditive field (Directive 3): older clients ignore it; the\n`#[serde(default)]` lets any deserializer omit it."},"pageCount":{"type":"integer","minimum":0},"urls":{"type":"array","items":{"type":"string"}}}},"CurlResponse":{"type":"object","required":["statusCode","body","url","headers"],"properties":{"body":{"type":"string","description":"Response body content","example":"<!doctype html>..."},"error":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/FetchError","description":"Advisory metadata for a received HTTP 429, a capped body, or a body\nstream that failed after response headers arrived. The response remains\ndata because its HTTP status and headers are authoritative."}]},"headers":{"type":"object","description":"Lower-cased response headers. Repeated values are comma-joined.","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"statusCode":{"type":"integer","format":"int32","description":"HTTP status code from the fetched URL","example":200,"minimum":0},"url":{"type":"string","description":"The URL that was fetched","example":"https://example.com"}},"example":{"statusCode":200,"body":"<!doctype html>...","url":"https://example.com","headers":{"content-type":"text/html; charset=utf-8"}}},"DelegatedAsset":{"type":"object","description":"An asset returned to an operational Dispatcher. The root `ownerId` bearer is\nintentionally absent; operational configuration stays available.","required":["id","name","kind","currency","timezone","createdAt"],"properties":{"bookingFields":{"type":"array","items":{"$ref":"#/components/schemas/BookingFieldDef"}},"createdAt":{"type":"integer","format":"int64","minimum":0},"currency":{"type":"string"},"description":{"type":["string","null"]},"id":{"type":"string"},"kind":{"type":"string"},"name":{"type":"string"},"policy":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/BookingPolicy"}]},"promoCodes":{"type":"array","items":{"$ref":"#/components/schemas/PromoCode"}},"timezone":{"type":"string"}}},"DelegatedAssetWithWindows":{"type":"object","required":["asset","windows","windowCount"],"properties":{"asset":{"$ref":"#/components/schemas/DelegatedAsset"},"windowCount":{"type":"integer","format":"int32","minimum":0},"windows":{"type":"array","items":{"$ref":"#/components/schemas/Window"}}}},"DelegatedAuditEvent":{"type":"object","description":"A delegated audit row. Reservation/waitlist IDs are public mutation\ncapabilities and detail is free-form, potentially credential-bearing text,\nso neither crosses this non-root boundary. Dispatchers get their booking\ncapabilities from the operational asset/window/reservation surfaces.","required":["at","action"],"properties":{"action":{"type":"string"},"assetId":{"type":["string","null"]},"at":{"type":"integer","format":"int64","minimum":0},"windowId":{"type":["string","null"]}}},"DiagnosticSample":{"type":"object","description":"One concrete failing-URL example carried in [`CrawlDiagnostics::samples`].\n\nA bounded list of examples so an operator sees *which* URLs failed\nand *why*, not just a count. `detail` carries the HTTP status text or\ncontent-type when the reason has one.","required":["url","reason"],"properties":{"detail":{"type":["string","null"]},"reason":{"type":"string","description":"Reason tag, matching a `CrawlDiagnostics` counter: one of\n`httpError`, `nonHtml`, `fetchError`, `robotsBlocked`,\n`scopeFiltered`, `depthFiltered`, `indexError`. (Frontier sheds\ncarry no samples — they are counted in bulk, never per URL.)"},"url":{"type":"string"}}},"DiscountKind":{"type":"string","description":"How a [`PromoCode`] reduces a window's per-unit cost.","enum":["percent","fixed"]},"DiscoverySource":{"type":"string","description":"How a URL entered the bounded crawl sample.","enum":["target","sitemap","homepage-link","site-search"]},"Echo":{"type":"object","description":"JSON payload for the echo endpoint.\n\nMirrors the schema of `fsly::echo::Echo`, plus api-specific request-derived bot/fraud signals.","required":["data","method","version","scheme","uri","ip","host","query","headers","fingerprint","session","torExitNode","iCloudPrivateRelay","fraud","ja3"],"properties":{"data":{"type":"string","description":"Request body data (empty for GET requests)","example":""},"fingerprint":{"type":"string","description":"SHA-256 fingerprint derived from selected headers","example":"a1b2c3d4e5f6789..."},"fraud":{"type":"number","format":"float","description":"Fraud score (0.0 = trusted, 1.0 = highly suspicious). Combines cheap\nheader/UA/JA3-JA4 heuristics with the IP/TLS reputation feeds (datacenter,\nVPN, Spamhaus DROP, abuse.ch SSLBL JA3).","example":0.15},"headers":{"type":"object","description":"Request headers (sorted alphabetically)","additionalProperties":{"type":"array","items":{"type":"string"}},"propertyNames":{"type":"string"}},"host":{"type":"string","description":"Host header value","example":"api.example.com"},"iCloudPrivateRelay":{"type":"boolean","description":"True if IP is in iCloud Private Relay range","example":false},"ip":{"type":"string","description":"Client IP address","example":"203.0.113.42"},"ja3":{"type":"string","description":"JA3 TLS fingerprint hash (if available from upstream proxy)","example":"abc123def456..."},"method":{"type":"string","description":"HTTP method (GET, POST, PUT, DELETE, etc.)","example":"GET"},"query":{"type":"object","description":"Parsed query parameters","additionalProperties":{"type":"array","items":{"type":"string"}},"propertyNames":{"type":"string"}},"scheme":{"type":"string","description":"URL scheme (http or https)","example":"https"},"session":{"type":"string","description":"Session hash (fingerprint + IP + cookie)","example":"f6e5d4c3b2a1098..."},"torExitNode":{"type":"boolean","description":"True if IP is a known Tor exit node","example":false},"uri":{"type":"string","description":"Request URI path","example":"/echo.json"},"version":{"type":"string","description":"HTTP version","example":"HTTP/1.1"}},"example":{"data":"","method":"GET","version":"HTTP/1.1","scheme":"https","uri":"/echo.json","ip":"203.0.113.42","host":"api.example.com","query":{},"headers":{"user-agent":["Mozilla/5.0"]},"fingerprint":"a1b2c3d4e5f6...","session":"f6e5d4c3b2a1...","torExitNode":false,"iCloudPrivateRelay":false,"fraud":0.15,"ja3":"abc123..."}},"EncodingResponse":{"type":"object","description":"Response payload for `/encoding.json`.\n\nField names are camelCase to match the `/...json` API conventions.","required":["raw","charset","octal","decimal","hex","md5","sha1","sha256","hash","rawLength","byteLength","base64Encoded","base64UrlEncoded","base64Decoded","base64Valid","base64DecodedBytes","urlEncoded","urlDecoded","htmlEncoded","htmlDecoded","hexEncoded","hexDecoded","hexValid","hexDecodedBytes","jsonEscaped","jsonUnescaped","jsonValid","binary"],"properties":{"base64Decoded":{"type":"string","description":"Base64-decoded input as text. Empty when the input is not valid Base64\n**or** when it decodes to bytes that are not text in the applied\ncharset — read `base64Valid` and `base64DecodedBytes` to tell those\ntwo cases apart.","example":""},"base64DecodedBytes":{"type":"integer","description":"Number of bytes the input decoded to; `0` when `base64Valid` is false.\nA non-zero count with an empty `base64Decoded` means the input is valid\nBase64 carrying binary data rather than text.","example":0,"minimum":0},"base64Encoded":{"type":"string","description":"Base64-encoded representation of raw bytes, standard alphabet\n(RFC 4648 section 4), padded with `=`.","example":"SGVsbG8="},"base64UrlEncoded":{"type":"string","description":"Base64-encoded representation of raw bytes, URL- and filename-safe\nalphabet (RFC 4648 section 5: `-` and `_` for `+` and `/`) with padding\nomitted — the form JWT/JOSE (RFC 7515) mandates.","example":"SGVsbG8"},"base64Valid":{"type":"boolean","description":"Whether the input value parses as Base64 at all. The decoder accepts\nboth the standard and the URL-safe alphabet and tolerates missing\npadding, so this is `true` for far more inputs than it looks — any\nfour-character run of alphabet characters decodes to three bytes.","example":false},"binary":{"type":"string","description":"Raw bytes in base 2, eight digits per byte, space separated. Unsigned,\nunlike the signed `octal` / `decimal` / `hex` views above, because a\nnegative binary byte is not a thing anyone means by \"text to binary\".","example":"01001000 01100101"},"byteLength":{"type":"integer","description":"Byte count of the raw input under the applied charset. Differs from\n`rawLength` for any non-ASCII input (`ä` is one character, two bytes).","example":5,"minimum":0},"charset":{"type":"string","description":"Applied charset (may differ from requested if unsupported)","example":"UTF-8"},"decimal":{"type":"string","description":"Decimal representation of raw bytes","example":"[72, 101, 108, 108, 111]"},"hash":{"type":"integer","format":"int64","description":"FNV-1a 64-bit hash of the raw input. A non-cryptographic checksum for\nbucketing and change detection — deliberately named in the docs so\ncallers can reproduce it, and deliberately not a substitute for the\nSHA-256 above.","example":12345678901234,"minimum":0},"hex":{"type":"string","description":"Hexadecimal representation of raw bytes","example":"[48, 65, 6c, 6c, 6f]"},"hexDecoded":{"type":"string","description":"The input read *as* hex and decoded back to text. Tolerates whitespace,\na leading `0x`, and `:` or `-` separators. Empty when the input is not\nhex or does not decode to text — read `hexValid` and `hexDecodedBytes`\nto tell those apart, exactly as with Base64.","example":""},"hexDecodedBytes":{"type":"integer","description":"Number of bytes the input decoded to when read as hex; `0` when\n`hexValid` is false.","example":0,"minimum":0},"hexEncoded":{"type":"string","description":"Raw bytes as one contiguous lowercase hex string. This is the form\n`xxd -p`, checksums and wire dumps use — distinct from the bracketed,\nsigned, Kotlin-style `hex` field above.","example":"48656c6c6f"},"hexValid":{"type":"boolean","description":"Whether the input parses as hex. Requires an even number of hex digits\nafter separators are removed.","example":false},"htmlDecoded":{"type":"string","description":"HTML character references in the input resolved back to text. Covers\nevery numeric reference (`&#169;`, `&#xA9;`) plus the HTML 4.01\nLatin-1, special and common symbol named entities. An unrecognised\nentity is left exactly as it stands rather than dropped, so decoding\nnever silently loses text.","example":"Tom & \"Jerry\""},"htmlEncoded":{"type":"string","description":"HTML-escaped input: `&`, `<`, `>`, `\"` and `'` become character\nreferences, so the value is safe to interpolate into element text or a\nquoted attribute. `'` uses the numeric `&#39;` rather than `&apos;`,\nwhich HTML 4 does not define.","example":"Tom &amp; &quot;Jerry&quot;"},"jsonEscaped":{"type":"string","description":"The input as a complete, quoted JSON string literal, ASCII-safe: every\nnon-ASCII character is written as `\\uXXXX` (surrogate pairs above\nU+FFFF). Paste-ready into JSON, JavaScript, Java or any other format\nthat takes JSON string syntax — the surrounding quotes are included.","example":"\"Hello\""},"jsonUnescaped":{"type":"string","description":"The input read *as* a JSON string literal, with `\\n`, `\\t`, `\\uXXXX`\nand friends resolved. The surrounding quotes are optional. Empty when\nthe input is not a well-formed JSON string; see `jsonValid`.","example":"Hello"},"jsonValid":{"type":"boolean","description":"Whether the input parses as a JSON string literal.","example":true},"md5":{"type":"string","description":"MD5 hash as lowercase hex","example":"8b1a9953c4611296a827abf8c47804d7"},"octal":{"type":"string","description":"Octal representation of raw bytes","example":"[110, 145, 154, 154, 157]"},"raw":{"type":"string","description":"Original input value","example":"Hello"},"rawLength":{"type":"integer","description":"Character count of raw input (Unicode scalar values, not UTF-16 units)","example":5,"minimum":0},"sha1":{"type":"string","description":"SHA-1 hash as lowercase hex","example":"f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0"},"sha256":{"type":"string","description":"SHA-256 hash as lowercase hex","example":"185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"},"urlDecoded":{"type":"string","description":"URL/percent-decoded input","example":"Hello"},"urlEncoded":{"type":"string","description":"URL/percent-encoded representation","example":"Hello"}},"example":{"raw":"Hello","charset":"UTF-8","octal":"[110, 145, 154, 154, 157]","decimal":"[72, 101, 108, 108, 111]","hex":"[48, 65, 6c, 6c, 6f]","md5":"8b1a9953c4611296a827abf8c47804d7","sha1":"f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0","sha256":"185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969","hash":12345678901234,"rawLength":5,"byteLength":5,"base64Encoded":"SGVsbG8=","base64UrlEncoded":"SGVsbG8","base64Decoded":"","base64Valid":false,"base64DecodedBytes":0,"urlEncoded":"Hello","urlDecoded":"Hello","htmlEncoded":"Hello","htmlDecoded":"Hello","hexEncoded":"48656c6c6f","hexDecoded":"","hexValid":false,"hexDecodedBytes":0,"jsonEscaped":"\"Hello\"","jsonUnescaped":"","jsonValid":false,"binary":"01001000 01100101 01101100 01101100 01101111"}},"EngagementAggregate":{"type":"object","description":"Aggregate engagement depth over a window of visits — returned as\n`VisitStats.engagement` and rendered on the dashboard as a KPI row. Answers\n*\"how sticky was this site?\"* with three ROBUST numbers (medians rather\nthan means, so a couple of tab-open-for-hours outliers don't skew) plus one\nmarketing KPI (`engaged_rate_pct`).","required":["samples"],"properties":{"engagedRatePct":{"type":["number","null"],"format":"double","description":"Marketing KPI — share of samples that count as \"engaged\": time on page\n≥ 10 s, OR scroll depth ≥ 50 %, OR at least one interaction. In\npercent (0..=100)."},"p50Interactions":{"type":["integer","null"],"format":"int32","description":"Median interaction count per visit (0..3 for the three counters:\nclick, keydown, pointerdown).","minimum":0},"p50ScrollPct":{"type":["number","null"],"format":"double","description":"Median deepest scroll depth reached, as a percentage of document\nheight (0..=100)."},"p50TimeOnPageMs":{"type":["integer","null"],"format":"int64","description":"Median visible time on page (ms) across all samples in the window.\nClient-side already capped at 30 min, so no outlier truncation here.","minimum":0},"p75TimeOnPageMs":{"type":["integer","null"],"format":"int64","description":"P75 visible time on page (ms) — surfaces the tail (how long the\nstickiest quartile actually stayed).","minimum":0},"samples":{"type":"integer","format":"int64","description":"Total number of visits in the window that contributed at least one\nengagement field. When zero, all fields are `None`."}}},"EntropyResponse":{"type":"object","description":"Mirrors the Kotlin `Entropy` DTO shape, but served as `/entropy.json`.\n\nKotlin source (for reference):\n- uuid: UUID.randomUUID()\n- secureRandomLong: Random().nextLong()\n- secureRandomFloat: Random().nextFloat()\n- secureRandomGaussian: Random().nextGaussian()\n- secureRandomInt: Random().nextInt()\n- timestamp: Instant.now()\n\nRust note:\n- We intentionally do *not* use \"secure\" entropy sources; we stick to a fast RNG suitable for\n  non-cryptographic \"jitter\" / uniqueness.\n- Timestamp is ISO 8601 (RFC 3339) in UTC, which is the natural JSON representation in Rust.\n  (Kotlin/Java `Instant` also serializes as an ISO timestamp under typical Jackson configs.)","required":["uuid","secure_random_long","secure_random_float","secure_random_gaussian","secure_random_int","timestamp"],"properties":{"secure_random_float":{"type":"number","format":"float","description":"Random float in range [0.0, 1.0)","example":0.42},"secure_random_gaussian":{"type":"number","format":"double","description":"Random Gaussian-distributed double (mean=0, stddev=1)","example":-0.5678},"secure_random_int":{"type":"integer","format":"int32","description":"Random 32-bit signed integer","example":42},"secure_random_long":{"type":"integer","format":"int64","description":"Random 64-bit signed integer","example":1234567890123456789},"timestamp":{"type":"string","description":"Current timestamp as epoch milliseconds","example":"1704067200000"},"uuid":{"type":"string","description":"Random UUID v4","example":"550e8400-e29b-41d4-a716-446655440000"}},"example":{"uuid":"550e8400-e29b-41d4-a716-446655440000","secure_random_long":1234567890123456789,"secure_random_float":0.42,"secure_random_gaussian":-0.5678,"secure_random_int":42,"timestamp":"1704067200000"}},"FetchError":{"type":"object","description":"Structured MCP fetch failure. HTTP responses, including 4xx/5xx, are not\nfailures and remain [`CurlResponse`] values. This type is reserved for\nvalidation, SSRF, transport, and bounded-body failures.","required":["kind","code","message"],"properties":{"code":{"type":"string"},"kind":{"$ref":"#/components/schemas/FetchErrorKind"},"message":{"type":"string"},"retryAfter":{"type":["string","null"]},"status":{"type":["integer","null"],"format":"int32","minimum":0}}},"FetchErrorKind":{"type":"string","description":"JSON response payload (mirrors Kotlin `Curl`).","enum":["invalid_request","ssrf","dns","tls","timeout","rate_limited","body_cap","upstream","transport"]},"FetchedPage":{"type":"object","description":"Response DTO for fetched/indexed pages.\nMatches the Kotlin `FetchedPage` structure.","properties":{"body":{"type":["string","null"]},"id":{"type":["string","null"]},"labels":{"type":"array","items":{"type":"string"}},"siteId":{"type":["string","null"],"format":"uuid"},"thumbnail":{"type":["string","null"]},"title":{"type":["string","null"]},"updated":{"type":["string","null"]},"url":{"type":["string","null"]}}},"FieldType":{"type":"string","description":"The value shape of an owner-defined [`BookingFieldDef`]. `Select` constrains\nthe value to one of the field's `options`; the rest are free-form text /\nnumeric / boolean values validated on booking.","enum":["text","number","bool","select"]},"FindingDetail":{"type":"object","description":"A finding plus everything a reader needs to act on it.","properties":{"category":{"type":"string","description":"Report section this belongs to.","default":"pageQuality"},"docs":{"type":["string","null"],"description":"Primary specification or reference, when one exists.","default":null},"how":{"type":"string","description":"How to address it.","default":""},"id":{"type":"string","description":"Stable catalogue id, e.g. `header.csp.missing`. Safe to match on.","default":""},"message":{"type":"string","description":"The same sentence that appears in `findings`.","default":""},"severity":{"type":"string","description":"Advisory triage — independent of the frozen 0–100 score.","default":"medium"},"title":{"type":"string","description":"Short label for the finding kind.","default":""},"what":{"type":"string","description":"What the thing being reported actually is.","default":""},"why":{"type":"string","description":"Why it matters — the concrete consequence, not a platitude.","default":""}}},"FoundPage":{"type":"object","description":"Found page in search results.\nEquivalent to Kotlin `FoundPage`.","properties":{"body":{"type":["string","null"]},"labels":{"type":"array","items":{"type":"string"}},"thumbnail":{"type":["string","null"]},"title":{"type":["string","null"]},"urlRaw":{"type":["string","null"]}}},"HarExportQuery":{"type":"object","description":"Query parameters for the HAR export endpoint.\nAll parameters are optional. When none are provided, returns all HAR entries\n(up to the default limit).","properties":{"before":{"type":["string","null"],"description":"ISO 8601 datetime: only include entries crawled before this time"},"contentType":{"type":["string","null"],"description":"Filter by MIME type (e.g. \"text/html\")"},"limit":{"type":["integer","null"],"description":"Maximum entries to return (default: 100, max: 10000)","minimum":0},"maxTime":{"type":["number","null"],"format":"double","description":"Maximum response time in milliseconds"},"minTime":{"type":["number","null"],"format":"double","description":"Minimum response time in milliseconds"},"offset":{"type":["integer","null"],"description":"Number of entries to skip (default: 0)","minimum":0},"since":{"type":["string","null"],"description":"ISO 8601 datetime: only include entries crawled at or after this time"},"sort":{"type":["string","null"],"description":"Sort field: \"time\", \"status\", \"crawledAt\" (default); prefix with \"-\" for descending"},"status":{"type":["integer","null"],"format":"int32","description":"Filter by HTTP status code (e.g. 200, 404)","minimum":0},"url":{"type":["string","null"],"description":"URL prefix match (e.g. \"https://example.com/blog\")"}}},"HealthCheck":{"type":"object","description":"One stable weighted health check. `weight` is the model's nominal weight;\nonly pass/fail checks contribute to `evaluatedWeight`.","properties":{"id":{"type":"string","default":""},"reason":{"type":"string","default":""},"status":{"oneOf":[{"$ref":"#/components/schemas/HealthCheckStatus"}],"default":"unknown"},"weight":{"type":"integer","format":"int32","default":0,"minimum":0}}},"HealthCheckStatus":{"type":"string","enum":["pass","fail","notApplicable","unknown"]},"HealthEvaluationState":{"type":"string","enum":["complete","partial","notEvaluated"]},"HealthReport":{"type":"object","description":"Versioned successor to the frozen top-level health score.","properties":{"checks":{"type":"array","items":{"$ref":"#/components/schemas/HealthCheck"},"default":[]},"evaluatedWeight":{"type":"integer","format":"int32","default":0,"minimum":0},"evaluationReasons":{"type":"array","items":{"type":"string"},"default":[]},"evaluationState":{"oneOf":[{"$ref":"#/components/schemas/HealthEvaluationState"}],"default":"notEvaluated"},"score":{"type":["integer","null"],"format":"int32","default":null,"minimum":0},"scoreModelVersion":{"type":"integer","format":"int32","default":0,"minimum":0}}},"ImportResult":{"type":"object","description":"Counts of what an `import` wrote.","required":["assets","windows","reservations","members","webhooks"],"properties":{"assets":{"type":"integer","format":"int32","minimum":0},"members":{"type":"integer","format":"int32","minimum":0},"reservations":{"type":"integer","format":"int32","minimum":0},"webhooks":{"type":"integer","format":"int32","minimum":0},"windows":{"type":"integer","format":"int32","minimum":0}}},"Inspection":{"type":"object","description":"Response payload for `/inspect.json`.","required":["whois","echo"],"properties":{"echo":{"$ref":"#/components/schemas/Echo","description":"Request echo data including headers, fingerprint, and fraud signals"},"whois":{"$ref":"#/components/schemas/Whois","description":"Geolocation and ASN data for the client IP"}}},"Member":{"type":"object","description":"A role-scoped delegated credential for an owner's team. `id` is the member's\nbearer token — it is used *in place of the ownerId* in the manager URL and is\nalso the revoke handle. Additive to the single-bearer model: the root ownerId\nkeeps working and is the only credential that can mint/revoke members.","required":["id","role","label","createdAt"],"properties":{"createdAt":{"type":"integer","format":"int64","minimum":0},"id":{"type":"string"},"label":{"type":"string"},"role":{"$ref":"#/components/schemas/Role"}}},"ModifyReservation":{"type":"object","description":"JSON body to modify an existing active booking in place — change the seat\n`quantity`, replace the custom `fields`, and/or edit the `note`. Every field is\noptional (omit to leave unchanged; a `note` of `\"\"` clears it). A quantity\nincrease is capacity-checked (may 409 `slot_full`); a decrease frees seats and\nmay auto-promote a waitlister. Cost/currency/promo stay as booked.","properties":{"fields":{"type":["object","null"],"additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"note":{"type":["string","null"]},"quantity":{"type":["integer","null"],"format":"int32","minimum":0}}},"NewAsset":{"type":"object","description":"JSON body to create an asset (the server mints `id`/`ownerId`/`createdAt`).","required":["name"],"properties":{"bookingFields":{"type":"array","items":{"$ref":"#/components/schemas/BookingFieldDef"},"description":"Owner-defined fields a consumer fills at booking (empty = none)."},"currency":{"type":"string"},"description":{"type":["string","null"]},"kind":{"type":"string"},"name":{"type":"string"},"policy":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/BookingPolicy","description":"Optional booking/cancellation policy."}]},"promoCodes":{"type":"array","items":{"$ref":"#/components/schemas/PromoCode"},"description":"Owner-defined discount codes (empty = none)."},"timezone":{"type":"string","description":"IANA timezone the slots are read in (default `UTC`)."}}},"NewAvailability":{"type":"object","description":"JSON body to bulk-generate a recurring grid of windows. Timezone-free: all\ntimes are absolute epoch-ms. Example — eight 1-hour slots each day for a week:\n`{firstStart, slotMinutes:60, slots:8, repeatEveryMinutes:1440, repeatCount:7, cost, capacity}`.","required":["firstStart","slotMinutes","slots","cost"],"properties":{"capacity":{"type":"integer","format":"int32","minimum":0},"cost":{"type":"number","format":"double"},"firstStart":{"type":"integer","format":"int64","description":"Epoch-ms start of the first slot.","minimum":0},"repeatCount":{"type":"integer","format":"int32","description":"Number of repetitions (0/1 = the block once).","minimum":0},"repeatEveryMinutes":{"type":"integer","format":"int32","description":"Offset between repetitions, minutes (0 = no repetition — a single block).","minimum":0},"slotMinutes":{"type":"integer","format":"int32","description":"Length of each slot, minutes.","minimum":0},"slots":{"type":"integer","format":"int32","description":"Number of back-to-back slots per repetition.","minimum":0}}},"NewHold":{"type":"object","description":"JSON body to place a **tentative hold** on a seat. Like [`NewReservation`] but\nthe booking auto-releases after `ttlSeconds` unless promoted with `confirm`.","required":["consumer","ttlSeconds"],"properties":{"consumer":{"type":"string"},"fields":{"type":"object","description":"Values for the asset's custom booking fields, keyed by field `key`.","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"note":{"type":["string","null"]},"promoCode":{"type":["string","null"],"description":"Optional discount code (case-insensitive)."},"quantity":{"type":"integer","format":"int32","description":"Units/seats to hold in this one call (default 1).","minimum":0},"ttlSeconds":{"type":"integer","format":"int64","description":"Seconds the hold lives before it auto-releases the seat\n(`HOLD_TTL_MIN_SECS`..=`HOLD_TTL_MAX_SECS`).","minimum":0}}},"NewMember":{"type":"object","description":"JSON body to mint a member token (server mints `id`/`createdAt`).","required":["role"],"properties":{"label":{"type":"string","description":"Optional human label (defaults to the role name)."},"role":{"type":"string","description":"`dispatcher` (read + book + schedule) or `viewer` (read-only)."}}},"NewReservation":{"type":"object","description":"JSON body a consumer POSTs to book a window.","required":["consumer"],"properties":{"consumer":{"type":"string"},"fields":{"type":"object","description":"Values for the asset's custom booking fields, keyed by field `key`.","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"note":{"type":["string","null"]},"promoCode":{"type":["string","null"],"description":"Optional discount code (case-insensitive); applies the asset's matching\n[`PromoCode`] to the snapshotted per-unit cost."},"quantity":{"type":"integer","format":"int32","description":"Units/seats to reserve in this one call (default 1; must fit the window's\nremaining capacity and not exceed its total capacity).","minimum":0}}},"NewWaitlist":{"type":"object","description":"JSON body to join a window's waitlist.","required":["consumer"],"properties":{"consumer":{"type":"string"},"fields":{"type":"object","description":"Custom booking-field values to carry onto the promoted hold.","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"note":{"type":["string","null"]},"quantity":{"type":"integer","format":"int32","description":"Seats wanted (default 1; must not exceed the window's total capacity).","minimum":0}}},"NewWebhook":{"type":"object","description":"JSON body to register a webhook (server mints `id`/`createdAt`, sanitizes `url`).","required":["url"],"properties":{"events":{"type":"array","items":{"type":"string"},"description":"Empty ⇒ subscribe to everything (`[\"*\"]`)."},"url":{"type":"string"}}},"NewWindow":{"type":"object","description":"JSON body to add a single utilization window (server mints `id`).","required":["start","end","cost"],"properties":{"capacity":{"type":"integer","format":"int32","description":"How many can book this slot; defaults to 1.","minimum":0},"cost":{"type":"number","format":"double"},"end":{"type":"integer","format":"int64","minimum":0},"start":{"type":"integer","format":"int64","minimum":0}}},"OutboundAggregate":{"type":"object","description":"Aggregate outbound clicks & downloads over a window of visits — returned\nas `VisitStats.outbound` and rendered on the dashboard as two side-by-side\nTOP-N panels. Answers *\"which off-site destinations do my visitors care\nabout, and what are they downloading?\"* with counted-frequency tables.\n\nRows are per-*event* (not per-visit): a single visit that clicked three\noutbound links contributes three to `total_outbound_clicks` and can appear\nthree times in `top_hosts`.","required":["topHosts","topDownloads","totalOutboundClicks","totalDownloads","samples"],"properties":{"samples":{"type":"integer","format":"int64","description":"Total number of visits in the window that opted into `?outbound=true`\n(whether or not they actually clicked anything)."},"topDownloads":{"type":"array","items":{"$ref":"#/components/schemas/OutboundCountRow"},"description":"Top download file extensions by event count (descending), capped at\n[`TOP_N`]. Empty when no outbound event carried a download extension."},"topHosts":{"type":"array","items":{"$ref":"#/components/schemas/OutboundCountRow"},"description":"Top external destination hosts by event count (descending), capped at\n[`TOP_N`]. Empty when no outbound event carried a host."},"totalDownloads":{"type":"integer","format":"int64","description":"Total download events (i.e. events with a `downloadExt` set) in the\nwindow. Same double-counting note as `total_outbound_clicks`."},"totalOutboundClicks":{"type":"integer","format":"int64","description":"Total outbound-click events (i.e. events with a `host` set) in the\nwindow. A download from an external host contributes to both this\ntotal *and* `total_downloads`."}}},"OutboundCountRow":{"type":"object","description":"One row of a TOP-N outbound breakdown: a label (external destination host,\nor a lowercased download file extension) with the count of matching events\nin the window. Simpler than [`CountRow`] because the outbound aggregation\nis per-event (not per-visit), so unique-visitor and human/bot splits don't\napply — an outbound click is either recorded or not.","required":["label","count"],"properties":{"count":{"type":"integer","format":"int64","description":"Count of matching events in the window."},"label":{"type":"string","description":"External destination host (e.g. `github.com`) or download file\nextension (e.g. `pdf`), lowercased."}}},"OutboundEvent":{"type":"object","description":"One outbound-click sample: an anchor click that pointed to another host\n(`host` set), or downloaded a known file type (`download_ext` set), or\nboth. Emitted zero-or-more times per visit; batched into\n[`PageOutbound::events`] and beaconed at visit end.\n\nNever contains the raw href — only the destination host (used for\nTOP-N-hosts breakdowns) and/or the lowercased file extension (used for\nTOP-N-downloads breakdowns). URL paths, query strings, and fragments are\ndropped in the browser before the beacon.","properties":{"downloadExt":{"type":["string","null"],"description":"The download file extension (lowercased, no dot; e.g. `pdf`) when\nthe href path ends with a known download extension; `None` for a\nbare external-navigation click.","default":null},"host":{"type":["string","null"],"description":"The destination host (e.g. `github.com`) when the click leaves the\ncurrent site; `None` for same-host downloads. Lowercased.","default":null}}},"OwnerExport":{"type":"object","description":"A complete, portable snapshot of an owner's data — assets (with their windows\nand bookings), members, and webhooks. The operator owns this: back it up, or\nre-`import` it into a fresh owner on another self-hosted instance (ids are\npreserved, so share links and booking receipts keep working). Audit log and\nwaitlist queues (transient) are intentionally excluded.","required":["version","ownerId","exportedAt"],"properties":{"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetWithWindows"}},"exportedAt":{"type":"integer","format":"int64","minimum":0},"members":{"type":"array","items":{"$ref":"#/components/schemas/Member"}},"ownerId":{"type":"string"},"version":{"type":"integer","format":"int32","minimum":0},"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}}}},"OwnerReport":{"type":"object","description":"An owner utilization + revenue report — pure aggregation over the owner's\nassets/windows/reservations (no new storage). Snapshot at `generatedAt`.","required":["assets","windows","seatsTotal","seatsBooked","utilizationPct","bookings","revenueByCurrency","upcoming","past","generatedAt"],"properties":{"assets":{"type":"integer","format":"int32","description":"Number of assets the owner publishes.","minimum":0},"bookings":{"$ref":"#/components/schemas/BookingCounts","description":"Booking counts by lifecycle state."},"generatedAt":{"type":"integer","format":"int64","description":"Epoch-ms the report was computed.","minimum":0},"past":{"type":"integer","format":"int32","description":"Active bookings whose window has already ended.","minimum":0},"revenueByCurrency":{"type":"object","description":"Realized/committed revenue (confirmed + completed) per currency; tentative\nholds are excluded.","additionalProperties":{"type":"number","format":"double"},"propertyNames":{"type":"string"}},"seatsBooked":{"type":"integer","format":"int64","description":"Seats consumed by active (non-cancelled, non-expired) bookings.","minimum":0},"seatsTotal":{"type":"integer","format":"int64","description":"Sum of window capacities (total bookable seats).","minimum":0},"upcoming":{"type":"integer","format":"int32","description":"Active bookings whose window starts in the future.","minimum":0},"utilizationPct":{"type":"number","format":"double","description":"`seatsBooked / seatsTotal * 100`, or `0` when there are no seats."},"windows":{"type":"integer","format":"int32","description":"Number of utilization windows across all assets.","minimum":0}}},"PageEncodingProvenance":{"type":"object","description":"Character-decoding evidence for a fetched HTML page. Raw byte size and hash\nremain byte-based; this only describes the decoded view used by analyzers.","properties":{"encoding":{"type":"string","description":"Canonical WHATWG encoding name, for example `UTF-8` or `windows-1251`.","default":""},"hadErrors":{"type":"boolean","description":"Whether malformed byte sequences required replacement characters.","default":false},"source":{"type":"string","description":"`bom`, `httpHeader`, `htmlMeta`, `xmlDeclaration`, or `fallback`.","default":""}}},"PageEngagement":{"type":"object","description":"Engagement-depth sample for one visit, opt-in via\n`/analytics.js?engagement=true`.\n\nA cheap \"how sticky was this pageview\" complement to Core Web Vitals — the\nthree fields answer *were they still there, how far did they read, did they\ntouch anything?* No DOM contents, no URLs, no ids; only structural counters\nthat describe the visit's shape. All fields optional so partial reports\n(tab closed pre-interaction, JS scroll disabled, etc.) still record.","properties":{"interactions":{"type":["integer","null"],"format":"int32","description":"Count of primary interactions during the visit: `click`,\n`keydown` (excluding modifier-only), and `pointerdown`. Deduped\nper-event-type so keyboard-repeat doesn't inflate the number.","default":null,"minimum":0},"scrollPct":{"type":["number","null"],"format":"double","description":"Deepest scroll reached during the visit, as a percentage of document\nheight (0..=100). `scrollY + innerHeight` over `scrollHeight`, ratcheted\nupward — never decreases on scroll-back.","default":null},"timeOnPageMs":{"type":["integer","null"],"format":"int64","description":"Wall-clock time the tab was foregrounded, ms — sum of visible spans\nbetween `visibilitychange`s. Capped at 30 min client-side to bound the\naggregation tail (a tab left open overnight is not engagement).","default":null,"minimum":0}}},"PageMeta":{"type":"object","description":"SEO / correctness facts extracted from the page's HTML `<head>` + body.\nPresence-and-shape only — no scoring here; the report folds the notable\ngaps into `findings`.","properties":{"canonical":{"type":["string","null"],"description":"`<link rel=canonical>` href.","default":null},"charsetDeclared":{"type":"boolean","description":"The document declares its character encoding in the markup: a\n`<meta charset>` or a `<meta http-equiv=content-type>` whose content\ncarries `charset=`. Pages relying on the Content-Type header alone are\nstill fine — [`quality_findings`] only flags the case where neither\ndeclares one.","default":false},"description":{"type":["string","null"],"description":"`<meta name=description>` content.","default":null},"h1Count":{"type":"integer","description":"Count of `<h1>` elements (0 or >1 are both SEO smells).","default":0,"minimum":0},"jsonLd":{"type":"boolean","description":"At least one `<script type=application/ld+json>` structured-data block.","default":false},"lang":{"type":["string","null"],"description":"`<html lang>` value.","default":null},"mixedContent":{"type":"array","items":{"type":"string"},"description":"`http://` sub-resource URLs referenced from an `https://` page — active\nmixed content the browser will block or downgrade the padlock over.\nCapped to a handful for report compactness.","default":[]},"ogImage":{"type":"boolean","description":"Open Graph image (`og:image`) present.","default":false},"ogTitle":{"type":"boolean","description":"Open Graph title (`og:title`) present.","default":false},"robots":{"type":["string","null"],"description":"`<meta name=robots>` content (e.g. `noindex,nofollow`).","default":null},"title":{"type":["string","null"],"default":null},"viewport":{"type":"boolean","description":"A responsive `<meta name=viewport>` is present.","default":false}}},"PageOutbound":{"type":"object","description":"Outbound-click / download batch for one visit, opt-in via\n`/analytics.js?outbound=true`. Beaconed once at\n`visibilitychange -> hidden` / `pagehide` (same pattern as vitals and\nengagement) so a single flush captures the whole visit.\n\n`events` may be empty — the collector still beacons at least once so the\naggregation counts the visit as \"opted in but had zero outbound events\",\ndistinguishing it from a site that never opted in at all.","properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/OutboundEvent"},"description":"Zero-or-more outbound events accumulated during the visit.","default":[]}}},"PageVitals":{"type":"object","description":"Core Web Vitals sample for one visit, opt-in via `/analytics.js?vitals=true`.\n\nThe four fields are Google's field-metric quartet (LCP + INP + CLS + TTFB);\nall are optional because a visit may report a partial set — e.g. the tab is\nclosed before INP fires, or the page has zero interactions. Absent values\nare simply dropped from the aggregate; no imputation. Numeric ranges follow\nGoogle's own definitions: LCP / INP / TTFB in milliseconds; CLS unitless.\nOnly structural / performance data; **no** DOM contents, no URLs, no ids.","properties":{"cls":{"type":["number","null"],"format":"double","description":"Cumulative Layout Shift (unitless, typically 0..1).","default":null},"inpMs":{"type":["number","null"],"format":"double","description":"Interaction to Next Paint (ms) — worst input-response latency in the\nsession. Absent on visits with zero real interactions.","default":null},"lcpMs":{"type":["number","null"],"format":"double","description":"Largest Contentful Paint (ms) — main-content load time.","default":null},"ttfbMs":{"type":["number","null"],"format":"double","description":"Time to First Byte (ms) — network + server first-byte time from\n`performance.getEntriesByType('navigation')[0].responseStart`.","default":null}}},"ProbeResult":{"type":"object","description":"One probed URL's result.","properties":{"class":{"type":"string","description":"Classification: `present` / `missing` (404) / `redirected` (3xx) /\n`soft-404` (200 + HTML where a non-HTML well-known file is expected) /\n`stale` (a 2xx security.txt past its RFC 9116 `Expires` timestamp) /\n`error` (4xx–5xx) / `unreachable` (no response).","default":""},"contentType":{"type":["string","null"],"default":null},"error":{"type":["string","null"],"description":"Set instead of a status when the probe could not complete.","default":null},"errorKind":{"type":["string","null"],"description":"Stable, additive classification for a transport failure. When no\nresponse arrived, the legacy `error: \"fetch_error\"` value remains\nunchanged for compatibility. A failure after response headers instead\nretains the status, sets `truncated`, and records its kind here.","default":null},"finalUrl":{"type":["string","null"],"description":"The URL the probe actually landed on, set **only** when the client\nfollowed one or more redirects and the final URL differs from the\nrequested one. Surfaces the redirect chain's endpoint (e.g. an\n`http→https` upgrade or a `www` canonicalisation) that a status-only\nview hides once the hops are auto-followed.","default":null},"headers":{"type":"object","description":"Curated response headers (lower-cased keys; see [`REPORTED_HEADERS`]).","default":{},"additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"ok":{"type":"boolean","description":"True when a response arrived with a 2xx/3xx status.","default":false},"path":{"type":"string","description":"Logical label: `(page)` for the target, else the well-known path.","default":""},"sha256":{"type":["string","null"],"default":null},"size":{"type":["integer","null"],"default":null,"minimum":0},"status":{"type":["integer","null"],"format":"int32","default":null,"minimum":0},"truncated":{"type":"boolean","description":"True if the body hit [`MAX_PROBE_BYTES`] or its stream was interrupted;\nin either case the reported hash and size cover only the captured prefix.","default":false},"url":{"type":"string","description":"The absolute URL probed.","default":""}}},"PromoCode":{"type":"object","description":"An owner-defined discount code a consumer supplies at booking. Unlimited-use\nwithin an optional validity window; the discounted per-unit price is snapshotted\nonto the [`Reservation`] (`cost`) and the code recorded (`appliedCode`).\n(Per-code usage caps are a deferred additive extension.)","required":["code","amount"],"properties":{"amount":{"type":"number","format":"double","description":"Percentage (`0..=100`) for `percent`, or a flat amount for `fixed`."},"code":{"type":"string","description":"Case-insensitive code the consumer types (stored upper-cased)."},"kind":{"$ref":"#/components/schemas/DiscountKind"},"validFrom":{"type":["integer","null"],"format":"int64","description":"Optional epoch-ms lower bound the code becomes valid (inclusive).","minimum":0},"validUntil":{"type":["integer","null"],"format":"int64","description":"Optional epoch-ms upper bound the code stays valid (exclusive).","minimum":0}}},"PublicWindow":{"type":"object","description":"A window as seen by a *consumer*: availability only — never a booker's details.","required":["id","start","end","cost","capacity","remaining","waitlisted","reserved"],"properties":{"capacity":{"type":"integer","format":"int32","minimum":0},"cost":{"type":"number","format":"double"},"end":{"type":"integer","format":"int64","minimum":0},"id":{"type":"string"},"remaining":{"type":"integer","format":"int32","minimum":0},"reserved":{"type":"boolean","description":"Convenience flag: `remaining == 0`."},"start":{"type":"integer","format":"int64","minimum":0},"waitlisted":{"type":"integer","format":"int32","description":"How many consumers are queued on this window's waitlist.","minimum":0}}},"RescheduleRequest":{"type":"object","description":"JSON body to reschedule a booking onto a different window of the same asset.","required":["windowId"],"properties":{"windowId":{"type":"string"}}},"Reservation":{"type":"object","description":"A consumer's booking of a window. `id` doubles as the booker's capability to\ncancel/reschedule it. Snapshots cost + currency at booking time.","required":["id","windowId","assetId","consumer","reservedAt","cost","currency"],"properties":{"appliedCode":{"type":["string","null"],"description":"The promo code applied to this booking, if any (upper-cased)."},"assetId":{"type":"string"},"consumer":{"type":"string","description":"Who booked — a free-form name or email the consumer supplies."},"cost":{"type":"number","format":"double","description":"Per-unit price snapshotted at booking time — **already discounted** if an\n`appliedCode` promo was used. Booking total = `cost * quantity`."},"currency":{"type":"string"},"fields":{"type":"object","description":"Consumer-supplied values for the asset's [`BookingFieldDef`]s (empty = none).","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"holdExpiresAt":{"type":["integer","null"],"format":"int64","description":"When a `Held` booking auto-releases (epoch-ms UTC). Set only while the\nreservation is a live hold; cleared once confirmed.","minimum":0},"id":{"type":"string","description":"Booking receipt id (UUID) — also the consumer's cancel/reschedule capability."},"note":{"type":["string","null"]},"quantity":{"type":"integer","format":"int32","description":"How many capacity units this booking consumes (group/quantity booking).\nDefaults to 1; `cost` is the per-unit window cost (total = `cost * quantity`).","minimum":0},"reservedAt":{"type":"integer","format":"int64","minimum":0},"status":{"$ref":"#/components/schemas/ReservationStatus"},"windowId":{"type":"string"}}},"ReservationRow":{"type":"object","description":"One row of the manager's booking list — a reservation with asset/window context.","required":["assetId","assetName","windowId","start","end","reservation"],"properties":{"assetId":{"type":"string"},"assetName":{"type":"string"},"end":{"type":"integer","format":"int64","minimum":0},"reservation":{"$ref":"#/components/schemas/Reservation"},"start":{"type":"integer","format":"int64","minimum":0},"windowId":{"type":"string"}}},"ReservationRowsResponse":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/ReservationRow"}},{"type":"array","items":{"$ref":"#/components/schemas/ViewerReservationRow"}}],"description":"Role-dependent serialization for the manager reservation list."},"ReservationStatus":{"type":"string","description":"The lifecycle state of a booking. `Confirmed`, `Completed` and (an unexpired)\n`Held` consume capacity; `Cancelled` and `Expired` free it. `Held` is a\n*tentative* reservation minted by [`AssetStore::hold`] that auto-releases at\nits `holdExpiresAt` — promoted to `Confirmed` by [`AssetStore::confirm`], or\nswept to `Expired` when its deadline passes. (Delivery-specific states —\ndispatched/delivered/failed — are a planned additive extension of this enum.)","enum":["confirmed","cancelled","completed","held","expired"]},"Role":{"type":"string","description":"The effective authority a manager-surface caller holds. The root `ownerId`\nresolves to [`Role::Owner`] (full control); a minted member token resolves to\nits scoped role. Only `Dispatcher`/`Viewer` are mintable as members.","enum":["owner","dispatcher","viewer"]},"SearchResult":{"type":"object","description":"Search result DTO.\nEquivalent to Kotlin `Result`.","required":["query","results"],"properties":{"query":{"type":"string"},"results":{"type":"array","items":{"$ref":"#/components/schemas/FoundPage"}}}},"SecurityAnalysis":{"type":"object","description":"Beyond mere presence: the *quality* of a page's security headers. A header\ncan be present yet toothless (a `Content-Security-Policy` allowing\n`unsafe-inline`, an `HSTS` with a one-second `max-age`), which a\npresence-only check scores identically to a strong one. This captures the\ndistinctions that matter and rolls them into a letter [`grade`].","properties":{"csp":{"type":"boolean","description":"`Content-Security-Policy` present at all.","default":false},"cspStyleUnsafeInline":{"type":"boolean","description":"The effective STYLE policy (`style-src`, else `default-src`) permits\n`unsafe-inline` — reported separately from the script-level loosening\nbecause inline styles are a far smaller exposure than inline scripts.","default":false},"cspUnsafeEval":{"type":"boolean","description":"`CSP` permits `unsafe-eval`.","default":false},"cspUnsafeInline":{"type":"boolean","description":"`CSP` permits `unsafe-inline` (a common XSS-defeating loosening).","default":false},"cspWildcard":{"type":"boolean","description":"`CSP` uses a wildcard source (`*`) in a fetch directive.","default":false},"grade":{"type":"string","description":"A..F letter grade over the weighted quality checks below.","default":""},"hsts":{"type":"boolean","description":"`Strict-Transport-Security` present at all.","default":false},"hstsIncludeSubdomains":{"type":"boolean","description":"`HSTS` carries `includeSubDomains`.","default":false},"hstsMaxAge":{"type":"integer","format":"int64","description":"`HSTS` `max-age` in seconds (0 when absent/unparseable).","default":0,"minimum":0},"https":{"type":"boolean","description":"Whether the audited target was reached over HTTPS.","default":false},"notes":{"type":"array","items":{"type":"string"},"description":"Human-readable quality issues (folded into the report findings).","default":[]},"permissionsPolicy":{"type":"boolean","description":"`Permissions-Policy` (feature policy) present.","default":false},"referrerPolicy":{"type":"boolean","description":"`Referrer-Policy` present.","default":false},"xContentTypeOptions":{"type":"boolean","description":"`X-Content-Type-Options: nosniff`.","default":false},"xFrameOptions":{"type":"boolean","description":"`X-Frame-Options` present (clickjacking defense; CSP `frame-ancestors`\nis the modern form but XFO is still widely honoured).","default":false}}},"SeoAnalysisMode":{"type":"string","enum":["source-html"]},"SeoCategory":{"type":"string","enum":["indexability","searchPresentation","siteLinkHygiene","structuredData","content","socialPreview","other"]},"SeoCheck":{"type":"object","description":"One stable, machine-comparable technical SEO check.","properties":{"category":{"oneOf":[{"$ref":"#/components/schemas/SeoCategory"}],"default":"other"},"evidence":{"type":"array","items":{"type":"string"},"default":[]},"id":{"type":"string","default":""},"message":{"type":"string","default":""},"recommendation":{"type":"string","default":""},"severity":{"oneOf":[{"$ref":"#/components/schemas/SeoSeverity"}],"default":"info"},"status":{"oneOf":[{"$ref":"#/components/schemas/SeoCheckStatus"}],"default":"notApplicable"}}},"SeoCheckChange":{"type":"object","description":"One stable technical SEO check's transition between comparable snapshots.","required":["id","kind"],"properties":{"afterStatus":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SeoCheckStatus"}]},"beforeStatus":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SeoCheckStatus"}]},"id":{"type":"string"},"kind":{"type":"string","description":"`introduced` / `resolved` / `status-changed`."}}},"SeoCheckStatus":{"type":"string","enum":["pass","fail","warning","opportunity","notApplicable"]},"SeoIndexPresence":{"type":"string","description":"Actual presence in a public search engine's index. Source-HTML Site Audit\nhas no authenticated engine evidence, so it must never infer this value\nfrom HTTP, robots, canonical, or sitemap signals.","enum":["notMeasured"]},"SeoIndexability":{"type":"string","description":"Technical indexing eligibility inferred from fetched page signals. In\nparticular, `Indexable` does not mean a search engine has indexed the URL.","enum":["indexable","excluded","blocked","unknown"]},"SeoMetrics":{"type":"object","description":"Advisory source-content metrics. None of these counts is a ranking factor or\ndirectly changes the technical score.","properties":{"emptyAnchorCount":{"type":"integer","default":0,"minimum":0},"emptyHeadingCount":{"type":"integer","default":0,"minimum":0},"externalLinkCount":{"type":"integer","default":0,"minimum":0},"hasByline":{"type":"boolean","default":false},"headingCount":{"type":"integer","default":0,"minimum":0},"headingOrderSkips":{"type":"integer","default":0,"minimum":0},"hreflangCount":{"type":"integer","default":0,"minimum":0},"imageCount":{"type":"integer","default":0,"minimum":0},"imagesMissingAlt":{"type":"integer","default":0,"minimum":0},"internalLinkCount":{"type":"integer","default":0,"minimum":0},"paragraphCount":{"type":"integer","default":0,"minimum":0},"visibleDateCount":{"type":"integer","default":0,"minimum":0},"wordCount":{"type":"integer","default":0,"minimum":0}}},"SeoReport":{"type":"object","description":"Separately versioned technical search-readiness result. This is not a\nranking score and never alters the legacy `AuditReport.score`.","properties":{"analysisMode":{"oneOf":[{"$ref":"#/components/schemas/SeoAnalysisMode"}],"default":"source-html"},"checks":{"type":"array","items":{"$ref":"#/components/schemas/SeoCheck"},"default":[]},"complete":{"type":"boolean","default":false},"expectedIndexable":{"type":"boolean","default":false},"indexPresence":{"oneOf":[{"$ref":"#/components/schemas/SeoIndexPresence"}],"default":"notMeasured"},"indexability":{"oneOf":[{"$ref":"#/components/schemas/SeoIndexability","description":"Technical eligibility inferred from fetched source signals. The legacy\nname is preserved for wire compatibility; it is not engine index state."}],"default":"unknown"},"metrics":{"oneOf":[{"$ref":"#/components/schemas/SeoMetrics"}],"default":{"headingCount":0,"paragraphCount":0,"wordCount":0,"emptyHeadingCount":0,"headingOrderSkips":0,"internalLinkCount":0,"externalLinkCount":0,"emptyAnchorCount":0,"imageCount":0,"imagesMissingAlt":0,"hreflangCount":0,"hasByline":false,"visibleDateCount":0}},"score":{"type":["integer","null"],"format":"int32","default":null,"minimum":0},"scoreModelVersion":{"type":"integer","format":"int32","default":0,"minimum":0},"sitemap":{"oneOf":[{"$ref":"#/components/schemas/SeoSitemapSummary"}],"default":{"membership":"unknown","complete":false,"documentsEvaluated":0,"entriesEvaluated":0}},"socialPreview":{"oneOf":[{"$ref":"#/components/schemas/SocialPreviewSummary"}],"default":{"openGraphTitle":false,"openGraphDescription":false,"openGraphImage":false,"openGraphUrl":false,"twitterCard":false,"twitterTitle":false,"twitterDescription":false,"twitterImage":false}},"structuredData":{"oneOf":[{"$ref":"#/components/schemas/StructuredDataSummary"}],"default":{"blocks":0,"validBlocks":0,"invalidBlocks":0,"truncated":false,"pageUrlSignalsTruncated":false,"items":0,"articleItems":0,"breadcrumbLists":0,"invalidBreadcrumbLists":0,"productItems":0,"softwareApplicationItems":0,"organizationItems":0,"offerItems":0}}}},"SeoSeverity":{"type":"string","enum":["high","medium","low","info"]},"SeoSitemapSummary":{"type":"object","description":"Compact, additive sitemap evidence for the audited page. This describes the\nalready-fetched conventional sitemap documents only; it does not claim a\nwhole-site crawl when an index or another declared sitemap was not fetched.","properties":{"complete":{"type":"boolean","default":false},"documentsEvaluated":{"type":"integer","default":0,"minimum":0},"entriesEvaluated":{"type":"integer","default":0,"minimum":0},"membership":{"oneOf":[{"$ref":"#/components/schemas/SitemapMembership"}],"default":"unknown"}}},"SiteConfig":{"type":"object","description":"Site configuration for crawling.\nEquivalent to Kotlin `SiteProfile.Config`.","properties":{"allowUrlWithQuery":{"type":"boolean"},"pageBodyCssSelector":{"type":"string"},"sitemapsOnly":{"type":"boolean"},"url":{"type":["string","null"],"description":"URL for the site configuration. Serializes as empty string if None."}}},"SiteCreation":{"type":"object","description":"Site creation response.\nEquivalent to Kotlin `SiteCreation`.","required":["siteId","siteSecret"],"properties":{"siteId":{"type":"string","format":"uuid"},"siteSecret":{"type":"string","format":"uuid"}}},"SiteIndexSummary":{"type":"object","description":"Site index summary response.\nEquivalent to Kotlin `SiteIndexSummary`.","required":["siteId","siteSecret","documents","failed","successCount"],"properties":{"documents":{"type":"array","items":{"type":"string"}},"failed":{"type":"array","items":{"type":"string"}},"siteId":{"type":"string","format":"uuid"},"siteSecret":{"type":"string","format":"uuid"},"successCount":{"type":"integer","minimum":0}}},"SitePage":{"type":"object","description":"Page content for search indexing.\nEquivalent to Kotlin `SitePage`.","required":["title","body","url","thumbnail"],"properties":{"body":{"type":"string"},"labels":{"type":"array","items":{"type":"string"}},"thumbnail":{"type":"string"},"title":{"type":"string"},"url":{"type":"string"}}},"SitePageInput":{"type":"object","description":"Input DTO for page upsert (PUT /sites/{siteId}/pages).\nMatches the Kotlin `SitePage` input structure with optional fields.","properties":{"body":{"type":["string","null"]},"id":{"type":["string","null"]},"labels":{"type":"array","items":{"type":"string"}},"siteId":{"type":["string","null"],"format":"uuid"},"thumbnail":{"type":["string","null"]},"title":{"type":["string","null"]},"updated":{"type":["string","null"]},"url":{"type":["string","null"]}}},"SiteProfile":{"type":"object","description":"Site profile configuration.\nEquivalent to Kotlin `SiteProfile`.","required":["id","secret"],"properties":{"configs":{"type":"array","items":{"$ref":"#/components/schemas/SiteConfig"}},"email":{"type":"string"},"id":{"type":"string","format":"uuid"},"secret":{"type":"string","format":"uuid"}}},"SiteProfileConfigInput":{"type":"object","description":"Site profile config input (matching Kotlin's SiteProfile.Config for input)","properties":{"allowUrlWithQuery":{"type":"boolean"},"pageBodyCssSelector":{"type":"string"},"sitemapsOnly":{"type":"boolean"},"url":{"type":"string"}}},"SiteProfileCreation":{"type":"object","description":"Site profile creation request.","properties":{"configs":{"type":"array","items":{"$ref":"#/components/schemas/SiteProfileConfigInput"}},"email":{"type":"string"}}},"SiteProfileUpdate":{"type":"object","description":"Site profile update request.\nMatches Kotlin `SiteProfileUpdate` which includes secret for updating credentials.","properties":{"configs":{"type":["array","null"],"items":{"$ref":"#/components/schemas/SiteProfileConfigInput"}},"email":{"type":["string","null"]},"secret":{"type":["string","null"],"format":"uuid","description":"New secret for the site (can be used to rotate credentials)"}}},"SitemapMembership":{"type":"string","description":"Whether the audited page's effective URL was observed in the bounded\nsitemap evidence available to the one-page audit. `Unknown` is deliberately\ndistinct from `Absent`: only a complete, valid sitemap corpus can prove\nabsence.","enum":["present","absent","duplicate","unknown"]},"SitesCrawlStatus":{"type":"object","description":"Collection of crawl statuses.\nEquivalent to Kotlin `SitesCrawlStatus`.","required":["sites"],"properties":{"sites":{"type":"array","items":{"$ref":"#/components/schemas/CrawlStatus"},"uniqueItems":true}}},"SnapshotDiff":{"type":"object","required":["before","after","scoreDelta","unchanged","changes"],"properties":{"after":{"$ref":"#/components/schemas/SnapshotMeta"},"before":{"$ref":"#/components/schemas/SnapshotMeta"},"cdnChange":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/CdnChange","description":"A CDN/edge-network transition, when the vendor verdict differs between\nthe two snapshots (origin-level; see [`CdnChange`])."}]},"changes":{"type":"array","items":{"$ref":"#/components/schemas/UrlChange"}},"scoreDelta":{"type":"integer","format":"int32","description":"`after.score - before.score`."},"seoCheckChanges":{"type":["array","null"],"items":{"$ref":"#/components/schemas/SeoCheckChange"},"description":"Stable-ID SEO check transitions. `None` means the snapshots' SEO\nanalyses are not comparable; `Some([])` means they are comparable and\nno check status changed."},"seoScoreDelta":{"type":["integer","null"],"format":"int32","description":"`after.seoScore - before.seoScore`, only for complete scored reports\nusing the same SEO model, expectation, and analysis mode. Old or\nincomparable snapshots omit it."},"unchanged":{"type":"integer","description":"Common paths whose probe was byte-for-byte unchanged.","minimum":0}}},"SnapshotMeta":{"type":"object","required":["id","capturedAt","target","host","score"],"properties":{"capturedAt":{"type":"integer","format":"int64","minimum":0},"host":{"type":"string"},"id":{"type":"string"},"label":{"type":["string","null"]},"score":{"type":"integer","format":"int32","minimum":0},"seoScore":{"type":["integer","null"],"format":"int32","description":"Technical SEO score, when this snapshot contains a complete scored SEO\nanalysis. Kept separate from the frozen compatibility score.","minimum":0},"seoScoreModelVersion":{"type":["integer","null"],"format":"int32","minimum":0},"target":{"type":"string"}}},"SocialPreviewSummary":{"type":"object","properties":{"openGraphDescription":{"type":"boolean","default":false},"openGraphImage":{"type":"boolean","default":false},"openGraphTitle":{"type":"boolean","default":false},"openGraphUrl":{"type":"boolean","default":false},"twitterCard":{"type":"boolean","default":false},"twitterDescription":{"type":"boolean","default":false},"twitterImage":{"type":"boolean","default":false},"twitterTitle":{"type":"boolean","default":false}}},"Stats":{"type":"object","required":["status","version","scmHash","buildNumber","uptimeS"],"properties":{"analyticsStats":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/AnalyticsStatsSummary","description":"Last successful analytics aggregate compute (absent until one ran).\nAdditive: the `redis-capacity` CI job pages when the compute time\ndrifts toward its ceiling. `scmHash`/`buildNumber` above stay pinned."}]},"buildNumber":{"type":"string","description":"CI build number injected via `BUILD_NUMBER` env var at runtime.\n\nCODEX Directive 3 contract: field is named `buildNumber` in public JSON.\nPinned by `api.hurl:77`."},"retentionSweep":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SweepSummary","description":"Last retention sweep tick (absent until one ran). Additive: the\n`redis-capacity` CI job pages when `lastOkUnixMs` goes stale, since the\nsweep is the only thing bounding visit growth."}]},"scmHash":{"type":"string","description":"Source-control hash (git commit) injected via `SCM_HASH` env var at runtime.\n\nCODEX Directive 3 contract: this field is named `scmHash` in the public\nJSON. Do not rename. `api.hurl:78` pins this as a published API promise."},"status":{"type":"string","description":"Service status — always `\"ok\"` if this endpoint responds."},"uptimeS":{"type":"integer","format":"int64","description":"Process uptime in seconds since api started.","minimum":0},"version":{"type":"string","description":"Crate version (from Cargo manifest)."}}},"StoredSnapshot":{"type":"object","description":"An immutable, labelled audit snapshot.","required":["id","capturedAt","report"],"properties":{"capturedAt":{"type":"integer","format":"int64","description":"Capture time, epoch milliseconds.","minimum":0},"id":{"type":"string"},"label":{"type":["string","null"]},"report":{"$ref":"#/components/schemas/AuditReport"}}},"StructuredDataSummary":{"type":"object","properties":{"articleItems":{"type":"integer","default":0,"minimum":0},"articleOpportunities":{"type":"array","items":{"type":"string"},"description":"Missing recommended Article-family properties, surfaced as opportunities.","default":[]},"articleWarnings":{"type":"array","items":{"type":"string"},"description":"Malformed values for present Article-family properties. Missing\nrecommended properties are tracked separately as advisory opportunities.","default":[]},"blocks":{"type":"integer","default":0,"minimum":0},"breadcrumbIssues":{"type":"array","items":{"type":"string"},"description":"Objective BreadcrumbList structure errors (required properties/order).","default":[]},"breadcrumbLists":{"type":"integer","default":0,"minimum":0},"commerceOpportunities":{"type":"array","items":{"type":"string"},"description":"Missing context-dependent rich-result property groups. Organization has\nno universally required group and therefore creates no omission issue.","default":[]},"commerceWarnings":{"type":"array","items":{"type":"string"},"description":"Malformed values on present Product, SoftwareApplication,\nOrganization, or Offer properties. These remain advisory so adding the\nvalidation does not alter the versioned score model.","default":[]},"invalidBlocks":{"type":"integer","default":0,"minimum":0},"invalidBreadcrumbLists":{"type":"integer","default":0,"minimum":0},"items":{"type":"integer","default":0,"minimum":0},"offerItems":{"type":"integer","default":0,"minimum":0},"offerVisibilityWarnings":{"type":"array","items":{"type":"string"},"description":"Material Offer literals observed only in strongly hidden source nodes.","default":[]},"organizationItems":{"type":"integer","default":0,"minimum":0},"pageUrlSignalsTruncated":{"type":"boolean","description":"True when more distinct current-page URL signals were present than can\nbe retained safely. The parent report is then incomplete and unscored.","default":false},"productItems":{"type":"integer","default":0,"minimum":0},"softwareApplicationItems":{"type":"integer","default":0,"minimum":0},"truncated":{"type":"boolean","description":"True when the bounded JSON-LD walk reached its nesting ceiling. The\nparent SEO report is then incomplete and deliberately unscored.","default":false},"types":{"type":"array","items":{"type":"string"},"default":[]},"validBlocks":{"type":"integer","default":0,"minimum":0}}},"SweepSummary":{"type":"object","description":"Published on `/stats.json` as `retentionSweep` so the `redis-capacity`\nalarm can tell a stalled sweep - the only thing bounding visit growth -\nfrom a healthy one.","required":["lastRunUnixMs","purged","sitesFailed","truncated"],"properties":{"error":{"type":["string","null"]},"lastOkUnixMs":{"type":["integer","null"],"format":"int64"},"lastRunUnixMs":{"type":"integer","format":"int64"},"purged":{"type":"integer","minimum":0},"sitesFailed":{"type":"integer","minimum":0},"truncated":{"type":"boolean"}}},"TlsInfo":{"type":"object","description":"TLS handshake + leaf-certificate facts for an HTTPS origin, captured by a\ndedicated pinned handshake (see [`crate::audit::tls`]). A tractable subset of\nwhat SSL Labs reports — enough for QA (expiry, key strength, hostname match,\nprotocol support) without the heavy machinery (full cipher enumeration,\nper-client handshake simulation, active vulnerability probes) tracked in the\nbacklog. Deliberately value-stable (no per-connection nonces) so re-audits\ndiff cleanly. Absent for `http://` targets or when the handshake failed.","properties":{"chain":{"type":"array","items":{"$ref":"#/components/schemas/ChainCert"},"description":"The additional certificates beyond the leaf (intermediates / roots).","default":[]},"chainLength":{"type":"integer","description":"Number of certificates the server presented (leaf + intermediates).","default":0,"minimum":0},"cipherSuite":{"type":["string","null"],"description":"The negotiated cipher suite (e.g. `TLS13_AES_256_GCM_SHA384`).","default":null},"configurationGrade":{"type":["string","null"],"description":"Protocol/configuration grade independent of certificate trust, hostname,\nand validity. `grade` remains the legacy combined result.","default":null},"daysRemaining":{"type":"integer","format":"int64","description":"Whole days until `notAfter` (negative once expired), from capture time.","default":0},"grade":{"type":["string","null"],"description":"A conservative TLS-config letter grade (A..F) over the subset we test —\nNOT the full Qualys rating (which needs cipher/legacy-protocol/vuln\nprobing; see the backlog).","default":null},"hostMatch":{"type":["boolean","null"],"description":"Whether the leaf covers the audited hostname (SAN, wildcard-aware).","default":null},"issuer":{"type":["string","null"],"description":"Issuer common-name / organization, best-effort.","default":null},"keyBits":{"type":["integer","null"],"format":"int32","description":"Leaf public-key strength in bits (RSA modulus / EC field size).","default":null,"minimum":0},"keyType":{"type":["string","null"],"description":"Leaf public-key type: `EC` / `RSA` / `Ed25519`.","default":null},"notAfter":{"type":"integer","format":"int64","description":"The leaf certificate's `notAfter`, epoch **seconds**.","default":0},"notBefore":{"type":"integer","format":"int64","description":"The leaf certificate's `notBefore`, epoch **seconds**.","default":0},"protocol":{"type":["string","null"],"description":"The highest TLS version negotiated (e.g. `TLSv1.3`).","default":null},"sans":{"type":"array","items":{"type":"string"},"description":"DNS and IP Subject Alternative Names on the leaf, rendered as strings.","default":[]},"signatureAlgorithm":{"type":["string","null"],"description":"Leaf certificate signature algorithm (e.g. `ecdsa-with-SHA384`).","default":null},"subject":{"type":["string","null"],"description":"Subject common-name, best-effort.","default":null},"supportedProtocols":{"type":"array","items":{"type":"string"},"description":"Which TLS versions completed a handshake (subset of `TLSv1.2`,`TLSv1.3`).","default":[]},"trustState":{"oneOf":[{"$ref":"#/components/schemas/TlsTrustState","description":"Public-root and hostname validation of the captured certificate chain.\n`unknown` means verification itself could not be performed; it is never\ntreated as trusted."}],"default":"unknown"},"validNow":{"type":"boolean","description":"True when the certificate is currently inside its validity window.","default":false}}},"TlsTrustState":{"type":"string","description":"Whether the presented certificate chain is trusted for the audited hostname\nby the bundled public WebPKI roots at capture time.","enum":["trusted","untrusted","unknown"]},"UpdateAsset":{"type":"object","description":"JSON body to edit an asset in place. Every field is optional — only the\nprovided fields change. A `description` of `\"\"` clears it; omitting it leaves\nit untouched. Ids/owner/createdAt are immutable.","properties":{"bookingFields":{"type":["array","null"],"items":{"$ref":"#/components/schemas/BookingFieldDef"},"description":"Replace the whole custom-field set (send `[]` to clear; omit to leave as-is)."},"currency":{"type":["string","null"]},"description":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"policy":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/BookingPolicy","description":"Set the policy (send `{minNoticeMinutes:0,cancelCutoffMinutes:0}` to clear)."}]},"promoCodes":{"type":["array","null"],"items":{"$ref":"#/components/schemas/PromoCode"},"description":"Replace the whole promo-code set (send `[]` to clear; omit to leave as-is)."},"timezone":{"type":["string","null"]}}},"UpdateWindow":{"type":"object","description":"JSON body to edit a window's `cost` and/or `capacity` in place (its time range\nis immutable — delete + re-add, or reschedule bookings, to move a slot).\n`capacity` may not drop below the window's current active-booking count.","properties":{"capacity":{"type":["integer","null"],"format":"int32","minimum":0},"cost":{"type":["number","null"],"format":"double"}}},"UrlChange":{"type":"object","description":"One URL's change between two snapshots.","required":["path","kind"],"properties":{"afterClass":{"type":["string","null"]},"beforeClass":{"type":["string","null"]},"details":{"type":"array","items":{"type":"string"}},"kind":{"type":"string","description":"`appeared` / `disappeared` / `broke` / `fixed` / `redirected` /\n`content-changed` / `header-changed` / `size-changed` / `status-changed`."},"path":{"type":"string"}}},"User":{"type":"object","required":["id","username"],"properties":{"id":{"type":"integer","format":"int64","description":"Unique user ID","example":1337,"minimum":0},"username":{"type":"string","description":"Username","example":"johndoe"}},"example":{"id":1337,"username":"johndoe"}},"ViewerAsset":{"type":"object","description":"An asset returned to a read-only Viewer. Promo codes are anonymous discount\ncapabilities and therefore stay on root/operational surfaces.","required":["id","name","kind","currency","timezone","createdAt"],"properties":{"bookingFields":{"type":"array","items":{"$ref":"#/components/schemas/BookingFieldDef"}},"createdAt":{"type":"integer","format":"int64","minimum":0},"currency":{"type":"string"},"description":{"type":["string","null"]},"id":{"type":"string"},"kind":{"type":"string"},"name":{"type":"string"},"policy":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/BookingPolicy"}]},"timezone":{"type":"string"}}},"ViewerAssetWithWindows":{"type":"object","required":["asset","windows","windowCount"],"properties":{"asset":{"$ref":"#/components/schemas/ViewerAsset"},"windowCount":{"type":"integer","format":"int32","minimum":0},"windows":{"type":"array","items":{"$ref":"#/components/schemas/ViewerWindow"}}}},"ViewerReservation":{"type":"object","description":"A booking visible to a read-only manager. All operational booking data is\nretained except the reservation bearer that can mutate the booking.","required":["windowId","assetId","consumer","status","quantity","reservedAt","cost","currency"],"properties":{"assetId":{"type":"string"},"consumer":{"type":"string"},"cost":{"type":"number","format":"double"},"currency":{"type":"string"},"fields":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"holdExpiresAt":{"type":["integer","null"],"format":"int64","minimum":0},"note":{"type":["string","null"]},"quantity":{"type":"integer","format":"int32","minimum":0},"reservedAt":{"type":"integer","format":"int64","minimum":0},"status":{"$ref":"#/components/schemas/ReservationStatus"},"windowId":{"type":"string"}}},"ViewerReservationRow":{"type":"object","required":["assetId","assetName","windowId","start","end","reservation"],"properties":{"assetId":{"type":"string"},"assetName":{"type":"string"},"end":{"type":"integer","format":"int64","minimum":0},"reservation":{"$ref":"#/components/schemas/ViewerReservation"},"start":{"type":"integer","format":"int64","minimum":0},"windowId":{"type":"string"}}},"ViewerWindow":{"type":"object","description":"A manager window whose embedded reservations cannot be used as public\ncancel/confirm/reschedule/modify capabilities.","required":["id","assetId","start","end","cost","capacity","remaining","waitlisted","reservations"],"properties":{"assetId":{"type":"string"},"capacity":{"type":"integer","format":"int32","minimum":0},"cost":{"type":"number","format":"double"},"end":{"type":"integer","format":"int64","minimum":0},"id":{"type":"string"},"remaining":{"type":"integer","format":"int32","minimum":0},"reservations":{"type":"array","items":{"$ref":"#/components/schemas/ViewerReservation"}},"start":{"type":"integer","format":"int64","minimum":0},"waitlisted":{"type":"integer","format":"int32","minimum":0}}},"ViewerWindowsPage":{"type":"object","required":["total","offset","windows"],"properties":{"offset":{"type":"integer","format":"int32","minimum":0},"total":{"type":"integer","format":"int32","minimum":0},"windows":{"type":"array","items":{"$ref":"#/components/schemas/ViewerWindow"}}}},"Visit":{"type":"object","description":"`/visit.json` verdict.","required":["verdict","score","reasons","ip","asName","asNumber","countryCode","country"],"properties":{"asName":{"type":"string","description":"Network owner (ASN org) — explains datacenter / proxy bot verdicts."},"asNumber":{"type":"integer","format":"int32","minimum":0},"country":{"type":"string"},"countryCode":{"type":"string"},"ip":{"type":"string","description":"Client IP used for the verdict (Envoy-derived, or `queryIP` override).","example":"1.1.1.1"},"reasons":{"type":"array","items":{"type":"string"},"description":"Human-readable signals behind the score."},"score":{"type":"integer","format":"int32","description":"Unified bot-likelihood, 0 (human) .. 100 (bot).","minimum":0},"verdict":{"type":"string","description":"`human` | `suspect` | `bot`."}}},"VisitDay":{"type":"object","description":"One UTC day's counts for the time series.","required":["day","visits","human","bot"],"properties":{"bot":{"type":"integer","format":"int64"},"day":{"type":"string","description":"`YYYY-MM-DD` (UTC)."},"human":{"type":"integer","format":"int64"},"visits":{"type":"integer","format":"int64"}}},"VisitStats":{"type":"object","description":"Aggregated visit analytics for one tracked site over a window —\nreturned by [`Metering::visit_stats`] / `GET /visit/stats.json`.","required":["siteId","since","totals","byDay","topPages","topReferrers","topCities","topCountries","topBrowsers","topSystems","topDevices","topNetworks","topJa3"],"properties":{"byDay":{"type":"array","items":{"$ref":"#/components/schemas/VisitDay"}},"engagement":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/EngagementAggregate","description":"Aggregate engagement depth across the window. `None` when no visit\nopted into `?engagement=true`."}]},"outbound":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/OutboundAggregate","description":"Aggregate outbound clicks & downloads across the window. `None` when\nno visit opted into `?outbound=true`."}]},"since":{"type":"string","description":"RFC3339 lower bound of the window."},"siteId":{"type":"string","description":"The tracked site id (serialized as `siteId`)."},"topBrowsers":{"type":"array","items":{"$ref":"#/components/schemas/CountRow"},"description":"Top visitor browsers in the window, classified from the User-Agent\n(descending), capped at [`TOP_N`]."},"topCities":{"type":"array","items":{"$ref":"#/components/schemas/CountRow"},"description":"Top visitor cities in the window (descending), capped at [`TOP_N`];\nunknown (unresolved) cities are excluded."},"topCountries":{"type":"array","items":{"$ref":"#/components/schemas/CountRow"},"description":"Top visitor countries (GeoIP ISO codes) in the window (descending),\ncapped at [`TOP_N`]; unknown (unresolved) countries are excluded."},"topDevices":{"type":"array","items":{"$ref":"#/components/schemas/CountRow"},"description":"Top visitor device types (Desktop / Mobile / Tablet) in the window,\nclassified from the User-Agent (descending), capped at [`TOP_N`]."},"topJa3":{"type":"array","items":{"$ref":"#/components/schemas/CountRow"},"description":"Top TLS fingerprints (JA3) in the window — a strong automation signal.\nAggregate only, with a [`JA3_MIN_VISITORS`] k-anonymity floor; empties\nexcluded. Shown as-is (not pseudonymised) to stay threat-intel-matchable."},"topNetworks":{"type":"array","items":{"$ref":"#/components/schemas/CountRow"},"description":"Top visitor networks (GeoIP ASN organization) in the window\n(descending), capped at [`TOP_N`]; unknown (unresolved) networks are\nexcluded."},"topPages":{"type":"array","items":{"$ref":"#/components/schemas/CountRow"},"description":"Most-visited paths in the window (descending), capped at [`TOP_N`]."},"topReferrers":{"type":"array","items":{"$ref":"#/components/schemas/CountRow"},"description":"Top external referrer hosts in the window (descending), capped at\n[`TOP_N`]; direct / same-site (empty-referrer) visits are excluded."},"topSystems":{"type":"array","items":{"$ref":"#/components/schemas/CountRow"},"description":"Top visitor operating systems in the window, classified from the\nUser-Agent (descending), capped at [`TOP_N`]."},"totals":{"$ref":"#/components/schemas/VisitTotals"},"vitals":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/VitalsAggregate","description":"Aggregate Core Web Vitals (P75 across the window). `None` when no\nvisit in the window opted into `?vitals=true` collection."}]}}},"VisitTotals":{"type":"object","description":"Verdict totals for one site over a window.","required":["visits","uniqueVisitors","human","suspect","bot","tor","relay","newVisits","returningVisits"],"properties":{"bot":{"type":"integer","format":"int64"},"human":{"type":"integer","format":"int64"},"newVisits":{"type":"integer","format":"int64","description":"Visits by a first-time identified visitor (Option B `loxal_vid` cookie) in\nthe window — the \"new\" side of the new-vs-returning split. Zero for\ncookieless (Option A) sites, which issue no visitor id."},"relay":{"type":"integer","format":"int64","description":"Visits via iCloud Private Relay (privacy-preserving real humans)."},"returningVisits":{"type":"integer","format":"int64","description":"Visits by a visitor seen before (Option B cookie) in the window — the\n\"returning\" side of the split."},"suspect":{"type":"integer","format":"int64"},"tor":{"type":"integer","format":"int64","description":"Visits over Tor exit nodes (anonymized) in the window."},"uniqueVisitors":{"type":"integer","format":"int64","description":"Distinct visitors (by client IP, cookieless) in the window."},"visits":{"type":"integer","format":"int64","description":"Total page views — one per recorded beacon — in the window."}}},"VitalsAggregate":{"type":"object","description":"Aggregate Core Web Vitals over the same window as [`VisitStats`]. Absent\n(`None`) when the window contains zero opt-in samples for a metric. `P75`\nis Google's canonical field-metric summary — it reflects the 75th-percentile\nexperience across all sampled visits, which is what Search Console reports.","required":["samples"],"properties":{"p75Cls":{"type":["number","null"],"format":"double","description":"P75 Cumulative Layout Shift (unitless) — Google: < 0.1 good, < 0.25\nneeds improvement, otherwise poor."},"p75InpMs":{"type":["number","null"],"format":"double","description":"P75 Interaction to Next Paint (ms) — Google: < 200 good, < 500 needs\nimprovement, otherwise poor."},"p75LcpMs":{"type":["number","null"],"format":"double","description":"P75 Largest Contentful Paint (ms) — Google: < 2500 good, < 4000 needs\nimprovement, otherwise poor."},"p75TtfbMs":{"type":["number","null"],"format":"double","description":"P75 Time to First Byte (ms) — Google (informational): < 800 good,\n< 1800 needs improvement."},"samples":{"type":"integer","format":"int64","description":"Total number of visits in the window that contributed at least one\nvitals metric. When zero, the four P75 fields are all `None`."}}},"WaitlistEntry":{"type":"object","description":"A consumer's place in a window's waitlist. `id` is their capability to leave\nthe queue. When a booking on the window is cancelled and frees a seat, the\nhead of the queue is auto-promoted into a [hold] and a `waitlist.promote`\nevent fires (so the operator can act on it).\n\n[hold]: AssetStore::hold","required":["id","windowId","assetId","consumer","joinedAt"],"properties":{"assetId":{"type":"string"},"consumer":{"type":"string"},"fields":{"type":"object","description":"Custom booking-field values carried onto the promoted hold (empty = none).","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"id":{"type":"string"},"joinedAt":{"type":"integer","format":"int64","minimum":0},"note":{"type":["string","null"]},"quantity":{"type":"integer","format":"int32","description":"Seats to promote into when this ticket reaches the head (default 1). The\nhead is only promoted when at least this many seats free.","minimum":0},"windowId":{"type":"string"}}},"Webhook":{"type":"object","description":"An outbound event webhook: the owner asks us to POST a JSON payload to `url`\nwhenever a matching event fires on their assets. `id` doubles as the delete\ncapability. `url` is SSRF-guarded at registration *and* at send.","required":["id","url","events","createdAt"],"properties":{"createdAt":{"type":"integer","format":"int64","minimum":0},"events":{"type":"array","items":{"type":"string"},"description":"Event selectors: `\"*\"` (all), a category (`booking`/`asset`/`window`/\n`owner`), or an exact action (`reserve`, `cancel`, `asset.create`, …)."},"id":{"type":"string"},"url":{"type":"string","description":"Sanitized target URL (http/https, web port, credentials stripped)."}}},"WhoAmI":{"type":"object","description":"The resolved identity behind a manager-surface token — lets a client tailor\nits UI to the caller's authority without leaking the owner secret.","required":["role","isOwner"],"properties":{"isOwner":{"type":"boolean","description":"True iff the caller holds the root ownerId."},"role":{"$ref":"#/components/schemas/Role"}}},"Whois":{"type":"object","description":"Whois response schema — stable across all data-source backends.\n\n**Why all 12 fields are present on every response, even when 5 of them are\nnot populated under the current default backend:**\n- The default data source is DB-IP Lite (CC BY 4.0). It carries 7 of these\n  12 fields with real values; the remaining 5 (`code` postal, `cityId`,\n  `countryState`, `countryStateId`, `timeZone`) come back with the\n  `EXTENDED_FIELD_HINT` / `EXTENDED_FIELD_HINT_ID` sentinel — telling\n  the consumer the field is part of the schema but only populated for\n  Pro / Enterprise customers.\n- The 5 hinted fields are positioned to customers as **extended fields\n  available on request** for Pro / Enterprise tiers (see\n  `site-kit/static/product/ip-intelligence.html`, which also hosts the folded `#pricing` section).\n  When a paying customer triggers a MaxMind GeoIP2 license purchase, the\n  data-source path for that key fills in those fields with real values —\n  no schema change required, no consumer breakage. CODEX Directive 3\n  wins via the response shape staying constant.","required":["ip","asName","asNumber","code","countryCode","timeZone","city","cityId","country","countryId","countryState","countryStateId"],"properties":{"asName":{"type":"string","description":"Autonomous System name (ISP/organization)","example":"Cloudflare, Inc."},"asNumber":{"type":"integer","format":"int32","description":"Autonomous System number","example":13335,"minimum":0},"city":{"type":"string","description":"City name","example":"Sydney"},"cityId":{"type":"integer","format":"int64","description":"GeoNames city ID. **Extended field** — `-1` (`EXTENDED_FIELD_HINT_ID`)\nunder the default backend; real positive integer on the upgrade.","example":-1},"code":{"type":"string","description":"Postal/ZIP code. **Extended field** — populated for Pro / Enterprise\ncustomers with the paid MaxMind GeoIP2 backend. Default backend\nreturns the [`EXTENDED_FIELD_HINT`] sentinel.","example":"<extended field — Pro/Enterprise: info@lifub.com>"},"country":{"type":"string","description":"Country name","example":"Australia"},"countryCode":{"type":"string","description":"ISO 3166-1 alpha-2 country code","example":"AU"},"countryId":{"type":"integer","format":"int64","description":"GeoNames country ID","example":2077456},"countryState":{"type":"string","description":"State / province ISO code (e.g. \"NSW\"). **Extended field** — see\n[`EXTENDED_FIELD_HINT`].","example":"<extended field — Pro/Enterprise: info@lifub.com>"},"countryStateId":{"type":"integer","format":"int64","description":"GeoNames state / province ID. **Extended field** — see\n[`EXTENDED_FIELD_HINT_ID`].","example":-1},"ip":{"type":"string","description":"Client IP address","example":"1.1.1.1"},"timeZone":{"type":"string","description":"IANA time zone identifier. **Extended field** — see [`EXTENDED_FIELD_HINT`].","example":"<extended field — Pro/Enterprise: info@lifub.com>"}},"example":{"ip":"1.1.1.1","asName":"Cloudflare, Inc.","asNumber":13335,"code":"<extended field — Pro/Enterprise: info@lifub.com>","countryCode":"AU","timeZone":"<extended field — Pro/Enterprise: info@lifub.com>","city":"Sydney","cityId":-1,"country":"Australia","countryId":2077456,"countryState":"<extended field — Pro/Enterprise: info@lifub.com>","countryStateId":-1}},"Window":{"type":"object","description":"A utilization window as returned to the manager: the slot plus its live\nbooking state (`remaining` seats + the reservations on it).","required":["id","assetId","start","end","cost","capacity","remaining","reservations"],"properties":{"assetId":{"type":"string"},"capacity":{"type":"integer","format":"int32","description":"How many can book this slot (default 1).","minimum":0},"cost":{"type":"number","format":"double","description":"Cost to use the asset for this window, in the asset's `currency`."},"end":{"type":"integer","format":"int64","description":"Slot end, epoch-ms UTC (exclusive); must be `> start`.","minimum":0},"id":{"type":"string"},"remaining":{"type":"integer","format":"int32","description":"Seats still available = `capacity` − active bookings.","minimum":0},"reservations":{"type":"array","items":{"$ref":"#/components/schemas/Reservation"},"description":"The bookings on this window (active + cancelled; `status` distinguishes)."},"start":{"type":"integer","format":"int64","description":"Slot start, epoch-ms UTC (inclusive).","minimum":0},"waitlisted":{"type":"integer","format":"int32","description":"How many consumers are queued on this window's waitlist.","minimum":0}}},"WindowsPage":{"type":"object","description":"A page of an asset's windows (owner drill-down; `total` is the full count).","required":["total","offset","windows"],"properties":{"offset":{"type":"integer","format":"int32","minimum":0},"total":{"type":"integer","format":"int32","minimum":0},"windows":{"type":"array","items":{"$ref":"#/components/schemas/Window"}}}},"WindowsPageResponse":{"oneOf":[{"$ref":"#/components/schemas/WindowsPage"},{"$ref":"#/components/schemas/ViewerWindowsPage"}],"description":"Role-dependent serialization for one asset's window page. Dispatchers need\nreservation IDs for their `Book` capability; viewers do not receive them."}},"securitySchemes":{"admin_secret":{"type":"http","scheme":"bearer","bearerFormat":"uuid","description":"The operator's admin secret."},"api_key":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Optional API key. Anonymous callers are rate-limited per IP; a key raises the limit and meters usage."},"audit_voucher":{"type":"apiKey","in":"header","name":"X-Site-Audit-Voucher","description":"Optional operator-issued Site Audit voucher UUID: adds paid reports and skips the proof of work."},"site_secret":{"type":"http","scheme":"bearer","bearerFormat":"uuid","description":"The site's secret UUID, issued when the site was created."},"site_secret_query":{"type":"apiKey","in":"query","name":"siteSecret","description":"The site's secret UUID, as a query parameter."}}},"tags":[{"name":"echo","description":"Request echo and fraud detection endpoints"},{"name":"entropy","description":"Random number generation"},{"name":"encoding","description":"Text encoding utilities (MD5, SHA1, SHA256, Base64)"},{"name":"whois","description":"IP geolocation and WHOIS lookup"},{"name":"inspect","description":"Request inspection utilities"},{"name":"curl","description":"HTTP fetch proxy"},{"name":"audit","description":"Bounded HTTP/security and technical SEO site audits"},{"name":"test","description":"Test endpoints"},{"name":"stats","description":"Build and deployment information"},{"name":"crawler","description":"Web crawler and site indexing API"},{"name":"sites","description":"Site management, search, and page operations"},{"name":"email","description":"Email notification endpoints"},{"name":"pages","description":"Direct page lookup by ID"},{"name":"assets","description":"Asset Manager (Beta — subject to change) — physical asset utilization & booking engine"},{"name":"visit","description":"Visit tracking beacon, Web Vitals, engagement, and per-site analytics"},{"name":"admin","description":"Operator-only crawler introspection"}]}