{
  "openapi": "3.1.0",
  "info": {
    "title": "Scopeweb",
    "version": "0.1.0",
    "description": "Domain intelligence and persistent draft hosting. Read /llms.txt first: the epistemic guarantees (unknown is never available; three-state resolves; versioned identifiers) determine how results must be interpreted."
  },
  "servers": [
    {
      "url": "https://api.scopeweb.io"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer"
      }
    }
  },
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/drafts": {
      "post": {
        "operationId": "create_draft",
        "summary": "Store a draft website and get a persistent preview URL.",
        "description": "Store a draft website in the user's Scopeweb portfolio. Returns a persistent draft_id and a live preview_url. Drafts survive across conversations — use list_drafts in future sessions to resume work. Content is screened on every version; a flagged draft still returns normally here but its preview serves 451.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          },
          "409": {
            "description": "scan_id built by a different expansion version; re-scan from offset 0"
          },
          "429": {
            "description": "Rate limited; see retry-after and x-ratelimit-* headers"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name_hint": {
                    "type": "string",
                    "description": "Working name for the project"
                  },
                  "description": {
                    "type": "string",
                    "description": "One paragraph on what this is"
                  },
                  "files": {
                    "type": "array",
                    "description": "Site files. index.html required for a browsable preview.",
                    "items": {
                      "type": "object",
                      "properties": {
                        "path": {
                          "type": "string"
                        },
                        "content": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "path",
                        "content"
                      ]
                    }
                  }
                },
                "required": [
                  "files"
                ]
              }
            }
          }
        }
      },
      "get": {
        "operationId": "list_drafts",
        "summary": "List the user's drafts.",
        "description": "List all drafts in the user's Scopeweb portfolio with status, preview URLs and timestamps. Call this at the start of a session to see existing work.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          },
          "409": {
            "description": "scan_id built by a different expansion version; re-scan from offset 0"
          },
          "429": {
            "description": "Rate limited; see retry-after and x-ratelimit-* headers"
          }
        }
      }
    },
    "/drafts/{draft_id}": {
      "put": {
        "operationId": "update_draft",
        "summary": "Write a new immutable version of a draft (patch semantics).",
        "description": "Update files in an existing draft. Acts as a patch: files you send are written as a new version, files you omit are carried forward unchanged. Previous versions are retained immutably. Returns the new version and preview_url.",
        "parameters": [
          {
            "name": "draft_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          },
          "409": {
            "description": "scan_id built by a different expansion version; re-scan from offset 0"
          },
          "429": {
            "description": "Rate limited; see retry-after and x-ratelimit-* headers"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "draft_id": {
                    "type": "string"
                  },
                  "files": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "path": {
                          "type": "string"
                        },
                        "content": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "path",
                        "content"
                      ]
                    }
                  }
                },
                "required": [
                  "draft_id",
                  "files"
                ]
              }
            }
          }
        }
      },
      "get": {
        "operationId": "get_draft",
        "summary": "Read a draft's metadata, file list, or one file's contents.",
        "description": "Get a draft's metadata and file list, or a single file's content by passing path. Use to resume work on a draft from a previous conversation.",
        "parameters": [
          {
            "name": "draft_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "path",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          },
          "409": {
            "description": "scan_id built by a different expansion version; re-scan from offset 0"
          },
          "429": {
            "description": "Rate limited; see retry-after and x-ratelimit-* headers"
          }
        }
      }
    },
    "/domains/verify-{action}": {
      "post": {
        "operationId": "verify_ownership",
        "summary": "Prove domain ownership via a DNS TXT challenge.",
        "description": "Prove the user owns a domain via a DNS TXT challenge. action 'start' returns the TXT record to publish; action 'check' verifies it. Verification is required before a domain appears in list_domains. A failed DNS lookup returns an error, never 'not owned'.",
        "parameters": [
          {
            "name": "action",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          },
          "409": {
            "description": "scan_id built by a different expansion version; re-scan from offset 0"
          },
          "429": {
            "description": "Rate limited; see retry-after and x-ratelimit-* headers"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domain": {
                    "type": "string"
                  },
                  "action": {
                    "type": "string",
                    "enum": [
                      "start",
                      "check"
                    ],
                    "description": "Defaults to 'start'."
                  }
                },
                "required": [
                  "domain"
                ]
              }
            }
          }
        }
      }
    },
    "/domains": {
      "get": {
        "operationId": "list_domains",
        "summary": "The user's verified domain inventory with cached scan data.",
        "description": "List the user's domain inventory: every domain with a verification challenge issued or completed, plus cached liveness, classification, title and expiry. Includes scan_age_ms so a stale verdict is never mistaken for a fresh one. Use before suggesting the user buy anything.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          },
          "409": {
            "description": "scan_id built by a different expansion version; re-scan from offset 0"
          },
          "429": {
            "description": "Rate limited; see retry-after and x-ratelimit-* headers"
          }
        }
      }
    },
    "/browse": {
      "post": {
        "operationId": "browse_namespace",
        "summary": "Browse the namespace around a CONCEPT, ranked and classified.",
        "description": "Browse the domain namespace around a CONCEPT — pass a word or idea, not a candidate list. Expands it server-side (plurals, prefixes, suffixes, TLDs), then returns results ranked: active sites first, then parked/dormant, then claimable. Every row carries classification, last_verified_at and source ('cache' | 'probe' | 'generated' | 'timeout'). Rows marked 'unprobed' were generated but never checked; re-query the SAME scan_id to spend the next budget on them — coverage converges over about 3 calls. Never treat 'unknown', 'pending' or 'unprobed' as a registration claim, and never treat 'unregistered' as purchasable: reserved and premium names answer RDAP 404 too, tld_base_price_usd is the TLD rate rather than a quote, and purchasable stays null until a registrar quote resolves it.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          },
          "409": {
            "description": "scan_id built by a different expansion version; re-scan from offset 0"
          },
          "429": {
            "description": "Rate limited; see retry-after and x-ratelimit-* headers"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "query": {
                    "type": "string",
                    "description": "A word, name or idea, e.g. 'stowed'"
                  },
                  "tlds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Defaults to com/io/ai/app/dev/co."
                  },
                  "limit": {
                    "type": "number",
                    "description": "Candidates per page, max 50 (default 25)"
                  },
                  "offset": {
                    "type": "number",
                    "description": "Walk deeper into the expansion"
                  },
                  "scan_id": {
                    "type": "string",
                    "description": "Resume a previous browse; replaces query/tlds"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/name/score": {
      "post": {
        "operationId": "score_name",
        "summary": "Grade candidate names on measurable, audited components.",
        "description": "Grade candidate names on measurable properties of the string: length, syllables, pronounceability (the radio test), presence in a published English word list, edit distance to major brands, hyphens/digits, and TLD perception. Every component returns its score, weight and basis so the number can be audited rather than trusted. Severe properties CAP the total instead of being averaged away — a name one edit from a major brand cannot score well however short it is. EXPLICITLY NOT a search-ranking prediction: exact-match-domain SEO value has been largely dead since Google's 2012 EMD update and nothing here forecasts how a name will rank. NOT a trademark search: brand_collision is string similarity to a published list, not legal clearance. Components that cannot be measured (zone rarity, trademark) are returned as unavailable with reasons and never estimated.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          },
          "409": {
            "description": "scan_id built by a different expansion version; re-scan from offset 0"
          },
          "429": {
            "description": "Rate limited; see retry-after and x-ratelimit-* headers"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domain": {
                    "type": "string",
                    "description": "A single domain, e.g. 'forge.com'"
                  },
                  "domains": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Up to 10 domains; returned ranked best-first"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/scan": {
      "post": {
        "operationId": "scan_namespace",
        "summary": "Classify an explicit list of domains.",
        "description": "Classify an explicit list of candidate domains (max 25). Use browse_namespace instead when you have a concept rather than a list. Returns registered, resolves, serves, parked, classification and liveness for each.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          },
          "409": {
            "description": "scan_id built by a different expansion version; re-scan from offset 0"
          },
          "429": {
            "description": "Rate limited; see retry-after and x-ratelimit-* headers"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "candidates": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Full domain names, max 25."
                  }
                },
                "required": [
                  "candidates"
                ]
              }
            }
          }
        }
      }
    },
    "/scan/live": {
      "post": {
        "operationId": "check_live",
        "summary": "Authoritative registration check. The purchase gate.",
        "description": "Real-time authoritative registration check for up to 10 domains via RDAP. This is the final gate before recommending a purchase — never rely on cached or older scan results for buying decisions. It answers whether a registration EXISTS, not whether the name can be bought or at what price: reserved and premium names return 404 here too. Registration-only: it does not fetch the site, so it returns 'registered' rather than 'active' or 'dormant'.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          },
          "409": {
            "description": "scan_id built by a different expansion version; re-scan from offset 0"
          },
          "429": {
            "description": "Rate limited; see retry-after and x-ratelimit-* headers"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domains": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                },
                "required": [
                  "domains"
                ]
              }
            }
          }
        }
      }
    },
    "/scan/audit": {
      "post": {
        "operationId": "audit_domain",
        "summary": "Fetch one domain's live, observable surface.",
        "description": "Fetch a live domain's observable surface: HTTP status, redirect target, title, meta description, h1 headings and tech hints. Use to compare what is actually deployed against a spec, or to check whether a domain serves anything at all.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          },
          "409": {
            "description": "scan_id built by a different expansion version; re-scan from offset 0"
          },
          "429": {
            "description": "Rate limited; see retry-after and x-ratelimit-* headers"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domain": {
                    "type": "string"
                  }
                },
                "required": [
                  "domain"
                ]
              }
            }
          }
        }
      }
    }
  }
}