{
  "name": "UXXRAY API",
  "version": "v1",
  "build": "23e30ee5e9c9",
  "description": "X-ray any funnel into measured facts — design tokens, proof language, and the exact conversion tactics it uses. Coverage reported as N of M patterns detected, zero LLM interpretation.",
  "base_url": "https://api.uxxray.com/api/v1",
  "endpoints": [
    {
      "method": "GET",
      "path": "/api/v1",
      "summary": "This self-describing API root — endpoint catalog, corpus counts, links"
    },
    {
      "method": "GET",
      "path": "/api/v1/openapi.json",
      "summary": "OpenAPI 3.1 document for this API"
    },
    {
      "method": "GET",
      "path": "/api/v1/league",
      "summary": "Ranked list of teardowns (?funnel_type=, ?limit=, ?offset=). Paginated: a full page does NOT mean the whole corpus — check the x-uxxray-has-more response header and follow Link rel=\"next\" until it reads false. Coverage is three fields and you must read `outcome` FIRST: `detected` (y, patterns found on this funnel) and `applicable` (x, patterns we looked for on it) are numbers only where outcome is \"Measured\", and null on \"Blocked\", \"Not measurable\" and \"No funnel found\" — a row we could not read publishes no count rather than a zero. `applicable` is THIS funnel's type-filtered denominator and is NOT the corpus-wide `tactics_total` that /api/v1/corpus/stats publishes"
    },
    {
      "method": "GET",
      "path": "/api/v1/lookup",
      "summary": "Find a company we have ALREADY x-rayed, by name or domain (?q=stripe, ?q=stripe.com, ?q=https://stripe.com/pricing). Returns the funnel instantly if we hold it, with the date it was measured and how many days ago. When we do not hold it the reply says so EXPLICITLY and hands back the scan URL: found:0 means \"not in our corpus\", never \"this company has no funnel\" and never \"the corpus is empty\". This is name resolution, NOT browsing: for design-task language use /api/v1/library/search, for the ranked corpus use /api/v1/league"
    },
    {
      "method": "GET",
      "path": "/api/v1/corpus/stats",
      "summary": "Corpus-wide aggregates (pct_lack, avg_tactics, evidence)"
    },
    {
      "method": "GET",
      "path": "/api/v1/patterns",
      "summary": "The Conversion Pattern Library — every detected tactic, prevalence, quoted evidence, examples (?funnel_type=)"
    },
    {
      "method": "GET",
      "path": "/api/v1/patterns/{pattern_id}",
      "summary": "One conversion pattern, machine-readable — up to 60 verbatim on-page examples"
    },
    {
      "method": "GET",
      "path": "/api/v1/templates",
      "summary": "Page Template Library — recurring section-skeletons real pages are built from, clustered by exact structure"
    },
    {
      "method": "GET",
      "path": "/api/v1/sections",
      "summary": "Section-archetype prevalence across fully-crawled pages — how common each band is, where it typically sits, example sites; ?archetype=X returns every real instance with screenshot crop coords (the SECTION layer)"
    },
    {
      "method": "GET",
      "path": "/api/v1/library/index",
      "summary": "THE WHOLE SHELF — every facet, every legal value, real counts, no specimens. Call this FIRST. The inventory is lumpy (hero is in the hundreds, stat_proof is single digits) and these counts are how you find that out before asking for 20 and getting 7"
    },
    {
      "method": "GET",
      "path": "/api/v1/library/specimens",
      "summary": "FACETED SPECIMEN RETRIEVAL — you choose the breadth. scope=narrow (5, full receipts incl. verbatim evidence) | broad (20, the band's cropped image + tactic ids — the DEFAULT) | full (the whole roster for the facet: identity + score only). Select on ONE axis: kind (+archetype/template) OR playbook; funnel_type filters either. Every response publishes how many exist, so \"showing 7 of 7 available\" can never be misread as a page of many. exclude_seen_project stops a later round re-serving what you already built from. A ranked response is ONE specimen per domain by default (score is funnel-scoped, so a 4-page walk would otherwise take 4 slots) and does not rank bands with no possible crop; both reductions are disclosed and per_domain= lifts the cap"
    },
    {
      "method": "GET",
      "path": "/api/v1/library/resolve",
      "summary": "DESIGN TASK → CORPUS VOCABULARY. \"build_review_section\" → the section archetypes, measured patterns and ready-made queries to use. Exact-match lookup: an unsupported task is reported as unsupported, never guessed"
    },
    {
      "method": "GET",
      "path": "/api/v1/library/aggregates",
      "summary": "WHAT THE CORPUS SAYS ABOUT AN ARCHETYPE — measured aggregates with denominators, not examples. Every figure is {value, n} where n is the instances that COULD answer that axis, never the population, so a caller cannot print a number without its coverage. An axis over fewer than 30 answerable instances states NOTHING and is reported in `withheld` with the count that fell short, so a thin answer reads as withheld rather than as a measured absence. Scope it with ?band= or ?min_score= before reading a share as advice: unfiltered, \"78% of heroes do X\" is a POPULARITY fact. Reduces over the same pool /api/v1/sections serves, which is the pool the public /section-design/ pages are built from, so the API and the published page cannot disagree. Every response carries `engines`, the distribution of crawler versions that actually produced the pool (each with its own n and date range), and `engine` is `mixed` whenever more than one contributed, so a caller can tell whether the figures are comparable before quoting them. Marketing-funnel design, copy and conventions only — not accessibility, performance or crawlability"
    },
    {
      "method": "GET",
      "path": "/api/v1/library/sections",
      "summary": "REQUIRES ?archetype= or ?task=; a bare call returns an error naming both. SECTION SPECIMENS with receipts — real bands from real funnels, each with crop_url (a real ~1440px webp of the band, or null with a stated reason) plus crop coords onto its full-page screenshot, the funnel's measured /100, and the verbatim on-page evidence for every tactic it runs. Ranked by score. The crops are cut at BUILD time over the listed corpus; scanning a site does not produce them (see steps[].crops_note on a bundle)"
    },
    {
      "method": "GET",
      "path": "/api/v1/library/elements",
      "summary": "ELEMENT SPECIMENS — the atomic UI pieces real funnels are built from (price anchors, ratings, guarantees, no-card notes, countdowns), each a REAL CROPPED IMAGE cut at BUILD time from the funnel's own full-page screenshot, plus the verbatim on-page text of that element. Scanning a site does not produce this material: a walk cuts at most two regions per step and they are a different tier (see steps[].crops_note on a bundle). Unlike section receipts, the text is element-scoped: the detector recorded the rect and the string from the same DOM node on the same crawl. Ranked by the funnel's measured /100"
    },
    {
      "method": "GET",
      "path": "/api/v1/library/pages",
      "summary": "PAGE SPECIMENS — one real crawled page per specimen, grouped by the MEASURED section sequence it shares with others (the shape). Carries the above-the-fold crop, the whole-page screenshot, the section skeleton the shape was clustered on, and the funnel's measured /100. Ranked by score"
    },
    {
      "method": "POST",
      "path": "/api/v1/visitors",
      "summary": "Issue a VISITOR CAPABILITY TOKEN — the continuity layer. It powers YOUR work (factory projects, likes) and is deliberately NOT an analytics identity: view counts deduplicate on a day-scoped IP hash and never read it. Returned ONCE and stored only as a SHA-256 hash, so it cannot be re-read. Send it back as the x-uxxray-visitor header — a header rather than a cookie so agents, which hold config but not cookies, use the same primitive as browsers"
    },
    {
      "method": "GET",
      "path": "/api/v1/visitors/me",
      "summary": "Your factory projects, resolved from the x-uxxray-visitor header. Returns a completeness note stating what is NOT listed — work from another device, from before the token existed, or created after clearing it is unreachable here and recoverable only by its own review link. Claiming full history would be a fabrication"
    },
    {
      "method": "GET",
      "path": "/api/v1/library/engagement",
      "summary": "Batch engagement counts for the specimens on one page. Fetched at RUNTIME and never baked into the static build, because a build-time number is a claim about when the page was generated dressed up as a current one. A ref with no row is OMITTED rather than returned as 0 — never counted and measured-zero are different facts. Carries the definition of the number"
    },
    {
      "method": "POST",
      "path": "/api/v1/library/engagement",
      "summary": "Record ONE deduped engagement event for a specimen. Deduplicated per visitor per day on a one-way, day-scoped IP hash — no cookie, no device storage, no identity. Agent visits COUNT (this is an agent-oriented product); 2000 pings in five seconds count once. Build, canary and internal traffic are excluded. The response carries the exact definition of the number, and returns recorded:false when the visitor was already counted today — that is a normal outcome, not an error"
    },
    {
      "method": "PATCH",
      "path": "/api/v1/factory/sessions/{session_id}",
      "summary": "Change a session's visibility and/or record sources discovered mid-work (#177) — body needs privacy, source_specimens, or both. REQUIRES the write credential (x-uxxray-session-token header): editing is an owner action, and the review link alone must not authorise it. private means UNLISTED, not access-controlled — the id is still a capability. source_specimens APPENDS to what is already stored, it never replaces — a worker that discovers a specimen it used in round 2 does not need to resend round 1's list"
    },
    {
      "method": "GET",
      "path": "/api/v1/library/search",
      "summary": "SPECIMEN SEARCH — free text resolved into explicit MEASURED filters (archetype, tactic, funnel type, min score), then real specimens with receipts. Deterministic, not semantic: any term it does not understand is REPORTED as unmatched rather than silently ignored"
    },
    {
      "method": "GET",
      "path": "/api/v1/library/brief",
      "summary": "THE INSPIRATION BRIEF — a SHORTLIST for one design task: resolved taxonomy, 2-3 selected specimens with receipts, what to read them for, and the originality instruction. Deterministic, so asking twice returns the same specimens; the response names its own pool size and links the full roster. For the whole library start at /api/v1/library/index instead"
    },
    {
      "method": "GET",
      "path": "/api/v1/playbooks",
      "summary": "Funnel-playbook registry + which crawled funnels match each (measured surface matches, quoted evidence; bundles carry per-funnel matches as bundle.playbooks)"
    },
    {
      "method": "GET",
      "path": "/api/v1/playbooks/{id}",
      "summary": "One funnel playbook, machine-readable: model summary + cheat sheet + how to unlock the prompt templates"
    },
    {
      "method": "GET",
      "path": "/api/v1/curriculum",
      "summary": "THE AGENT STUDY PATH (markdown) — how to learn conversion design from this corpus: top scorers → bundles → screenshots → playbook → page → section → copy"
    },
    {
      "method": "POST",
      "path": "/api/v1/rate",
      "summary": "★ Rate any result 1-5 — body {rating, context?, via: \"user\"(relayed human)|\"agent\", comment?}. Terminal responses carry the ask as _rate"
    },
    {
      "method": "GET",
      "path": "/api/v1/rate",
      "summary": "Rating summary — overall count/avg + breakdown by context"
    },
    {
      "method": "POST",
      "path": "/api/v1/scan-request",
      "summary": "Request a new site be scanned (queued for manual/batch capture) — body {url, email?}"
    },
    {
      "method": "POST",
      "path": "/api/v1/scan",
      "summary": "Live on-demand x-ray — real Browser Rendering capture, async. Body {url, mode?, steps?, maxSteps?, force?}. mode=\"auto\" (default) auto-discovers the funnel (entry → pricing/product → signup/checkout); \"single\" scans just the entry page; \"defined\" walks the exact same-domain URLs in steps[]. maxSteps caps auto/defined (default 4, max 6). Returns 202 {jobId,status} or 200 {status:done,slug} if cached within 24h; {\"force\":true} bypasses the cache."
    },
    {
      "method": "GET",
      "path": "/api/v1/scan/{jobId}",
      "summary": "Poll a live scan job — {status, slug?, score?, band?, blocked?, error?}"
    },
    {
      "method": "POST",
      "path": "/api/v1/playbooks/unlock",
      "summary": "Unlock a playbook's full prompt templates — body {email, playbook, source?}; returns {playbook:{prompts_md, cheat_md}}"
    },
    {
      "method": "POST",
      "path": "/api/v1/playbooks/feedback",
      "summary": "Report whether a playbook was useful — body {playbook, useful, comment?, email?, via?}"
    },
    {
      "method": "GET",
      "path": "/api/v1/factory/sessions",
      "summary": "List Design Factory sessions. Anonymous callers see ONLY sessions explicitly created with privacy:\"public\" — private sessions are reachable solely by someone who already holds the id"
    },
    {
      "method": "POST",
      "path": "/api/v1/factory/sessions",
      "summary": "Create a Design Factory session — PRIVATE by default; returns a write credential ONCE that is required to register concepts (header x-uxxray-session-token). The session_id alone is the review link: safe to hand a human, allows reading and feedback, not writing. Create — body {task, project?, target?, source_specimens?, agent?, privacy?}; external AI agents register concepts into it"
    },
    {
      "method": "GET",
      "path": "/api/v1/factory/sessions/{session_id}",
      "summary": "Get one Design Factory session, its registered concepts, latest feedback, and agent contract"
    },
    {
      "method": "POST",
      "path": "/api/v1/factory/sessions/{session_id}/concepts",
      "summary": "Register an original concept into a session. REQUIRES the write credential from session creation, sent as the x-uxxray-session-token header; the review link alone will not authorise it. artifact must carry non-empty html or css"
    },
    {
      "method": "GET",
      "path": "/api/v1/factory/sessions/{session_id}/feedback",
      "summary": "Get latest session-scoped feedback as both raw verdicts and an agent-ready refinement summary"
    },
    {
      "method": "POST",
      "path": "/api/v1/factory/sessions/{session_id}/feedback",
      "summary": "Record session-scoped concept feedback — body {concept_id, starred?, rating?, comment?, tags?}"
    },
    {
      "method": "PATCH",
      "path": "/api/v1/factory/sessions/{session_id}/concepts/{concept_id}",
      "summary": "Commit a round's winner (#159 PART B) — body {status:\"selected\"}. REQUIRES the write credential (x-uxxray-session-token header), same as concept registration: the review link alone must not decide a session. Marks this concept selected, archives every OTHER direction's root, stamps the session's decided_concept_id/decided_at, and returns round_two_seed — the winner's brief + comment, every REJECTED concept's comment labelled as such, the source specimens already used, and both round-two branches (evolve the winner vs fresh alternatives). The human chooses the branch, never the agent"
    },
    {
      "method": "GET",
      "path": "/api/v1/factory/sessions/{session_id}/concepts/{concept_id}/artifact",
      "summary": "Get the portable artifact bundle for one generated concept: HTML/CSS/tokens/adapters/source provenance when registered"
    },
    {
      "method": "POST",
      "path": "/api/v1/report/dispute",
      "summary": "Report a tactic we missed on a report — body {slug, tactic_id, url?, note?, email?, via?}; logged for detector review, not auto-applied"
    },
    {
      "method": "GET",
      "path": "/api/v1/teardown/{slug}",
      "summary": "Full teardown — all device variants, score, funnel steps"
    },
    {
      "method": "GET",
      "path": "/api/v1/teardown/{slug}/bundle",
      "summary": "One self-contained JSON with everything for one site (?include=, ?exclude=) — the recommended single fetch for agents"
    },
    {
      "method": "GET",
      "path": "/api/v1/teardown/{slug}/tokens",
      "summary": "Design tokens (?format=json|w3c|tailwind|css). The json form carries the five-slot `tokens` export UNCHANGED plus a `design` object holding what those slots were derived from: the full palette ordered by usage with usage_pct, weighted ink candidates, the real type scale and weights, EVERY radius, and the measured roles with any collisions NAMED rather than resolved by inventing a colour. Page-scoped. No spacing field — nothing measures spacing"
    },
    {
      "method": "GET",
      "path": "/api/v1/teardown/{slug}/brief",
      "summary": "Machine-generated build-brief markdown"
    },
    {
      "method": "GET",
      "path": "/api/v1/teardown/{slug}/favicon",
      "summary": "Favicon image, streamed from R2"
    },
    {
      "method": "GET",
      "path": "/api/v1/teardown/{slug}/report",
      "summary": "Pre-rendered shareable report HTML (self-heals from D1/R2 if not yet rendered)"
    },
    {
      "method": "GET",
      "path": "/api/v1/teardown/{slug}/og.png",
      "summary": "Branded 1200x630 social card image, composed on demand from the latest hero screenshot"
    },
    {
      "method": "GET",
      "path": "/api/v1/teardown/{slug}/screenshot/{path}",
      "summary": "One step screenshot/crop, streamed from R2 (e.g. path=\"01-home/hero.png\")"
    }
  ],
  "counts": {
    "teardowns": 2468,
    "scanned_domains": 2468
  },
  "links": {
    "league": "https://api.uxxray.com/api/v1/league",
    "openapi": "https://api.uxxray.com/api/v1/openapi.json",
    "llms": "https://uxxray.com/llms.txt",
    "mcp": "https://api.uxxray.com/mcp"
  },
  "note": "0-hallucination — every field is a measured fact or a deterministic score; no LLM interpretation. See provenance/source_type fields on /bundle."
}