{
  "openapi": "3.1.0",
  "info": {
    "title": "Sourcely Public API",
    "summary": "Find and analyze academic sources for text.",
    "description": "Sourcely Public API — initial create and get search contract. Preview: search records use Redis with one-hour terminal retention by default; searches execute in a Redis-backed worker and require a dashboard API key. Create returns 402 when the credit balance cannot cover the estimate.",
    "contact": {
      "name": "Sourcely",
      "url": "https://sourcely.net/"
    },
    "version": "0.0.1"
  },
  "servers": [
    {
      "url": "https://api.sourcely.net",
      "description": "Sourcely Public API"
    }
  ],
  "paths": {
    "/search": {
      "post": {
        "tags": [
          "search"
        ],
        "summary": "Create a search job",
        "operationId": "create_search_search_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SearchRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "src_example",
                  "status": "queued",
                  "usage": {
                    "currency": "USD",
                    "estimated_cost": 0.005,
                    "actual_cost": null,
                    "discount_percent": 0,
                    "balance_after": null
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "Prepaid balance is below this search's estimated cost. No search is created and no usage is reported.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "insufficient_balance",
                    "message": "Prepaid balance is below the estimated cost of this search.",
                    "details": {
                      "estimated_cost": 0.005,
                      "credits_remaining": 0.001
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Job not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Search state changed or an operation is already running",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Per-key rate limit exceeded. Wait for the Retry-After header.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until this API key can retry the limited operation.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Redis, API key verification, or rate limiting is temporarily unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "description": "Create a search and receive its job_id. Requires a Bearer API key. Returns 202 with status queued after saving the search and dispatching it to the Redis worker queue. Returns 402 when the dashboard balance cannot cover estimated_cost. When webhook_url is set, the worker POSTs the result after the search finishes. Poll the get operation for running, completed or failed status and the results."
      }
    },
    "/search/{job_id}": {
      "get": {
        "tags": [
          "search"
        ],
        "summary": "Get a search job's status + results",
        "operationId": "get_search_search__job_id__get",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Job Id"
            },
            "example": "src_example"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResult"
                },
                "example": {
                  "job_id": "src_example",
                  "status": "completed",
                  "result_type": "search",
                  "document": {
                    "source_ids": [
                      "src_paper_example"
                    ],
                    "scores": {
                      "src_paper_example": 1
                    },
                    "search_queries": [
                      "urban green space mental health outcomes",
                      "green space exposure depression anxiety"
                    ],
                    "page": 1,
                    "has_more": false,
                    "status": "done"
                  },
                  "claims": null,
                  "sources": {
                    "src_paper_example": {
                      "id": "src_paper_example",
                      "title": "Example study of urban green space and mental health",
                      "authors": [
                        "A. Example",
                        "B. Sample"
                      ],
                      "year": 2024,
                      "venue": "Example Research Journal",
                      "abstract": "Synthetic example used to illustrate the response structure.",
                      "url": "https://example.org/papers/urban-green-space",
                      "pdf_url": null,
                      "citation_count": null,
                      "score": 1,
                      "citations": null,
                      "enrichment": null,
                      "parsed_source": null
                    }
                  },
                  "usage": {
                    "currency": "USD",
                    "estimated_cost": 0.005,
                    "actual_cost": 0.005,
                    "discount_percent": 0,
                    "balance_after": null
                  },
                  "metadata": {
                    "requested_max_claims": 4,
                    "actual_max_claims": 0
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Job not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "job_not_found",
                    "message": "Job src_missing was not found.",
                    "details": null
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Per-key rate limit exceeded. Wait for the Retry-After header.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until this API key can retry the limited operation.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Redis, API key verification, or rate limiting is temporarily unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "description": "Read a previously created search by job_id using the owning account's API key. Returns status and available sources. Does not execute the search again. A failed search is currently returned as a 200 response with status failed. Another account receives 404."
      }
    }
  },
  "components": {
    "schemas": {
      "CitationFormat": {
        "type": "string",
        "enum": [
          "apa7",
          "mla9",
          "ieee",
          "harvard",
          "bibtex"
        ],
        "title": "CitationFormat",
        "description": "CitationFormat values in the current API contract."
      },
      "Citations": {
        "properties": {
          "apa7": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Apa7",
            "description": "APA 7 formatted citation, or null if not requested."
          },
          "mla9": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Mla9",
            "description": "MLA 9 formatted citation, or null if not requested."
          },
          "ieee": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Ieee",
            "description": "IEEE formatted citation, or null if not requested."
          },
          "harvard": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Harvard",
            "description": "Harvard formatted citation, or null if not requested."
          },
          "bibtex": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Bibtex",
            "description": "BibTeX entry, or null if not requested."
          }
        },
        "type": "object",
        "title": "Citations",
        "description": "Citations fields in the current API contract."
      },
      "Claim": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id",
            "description": "Identifier for this claim within the search."
          },
          "span": {
            "type": "string",
            "title": "Span",
            "description": "The identified claim text from the input."
          },
          "searched": {
            "type": "boolean",
            "title": "Searched",
            "description": "Whether a source search was attempted for this claim. Claims beyond max_claims can be returned with false."
          },
          "source_ids": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Source Ids",
            "description": "Ordered IDs pointing into the top-level sources map."
          },
          "scores": {
            "additionalProperties": {
              "type": "number"
            },
            "type": "object",
            "title": "Scores",
            "description": "Per-claim rank scores keyed by source ID; not probabilities."
          },
          "page": {
            "type": "integer",
            "title": "Page",
            "default": 1,
            "description": "Latest page fetched for this claim; initially 1."
          },
          "has_more": {
            "type": "boolean",
            "title": "Has More",
            "default": false,
            "description": "Heuristic indication of additional potential results. Claim refetch is deferred."
          },
          "status": {
            "$ref": "#/components/schemas/TrackStatus",
            "default": "pending",
            "description": "State of the claim track. Unsearched claims may remain pending after the job completes."
          },
          "error": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ErrorDetail"
              },
              {
                "type": "null"
              }
            ],
            "description": "Safe error details for a failed claim, when present."
          }
        },
        "type": "object",
        "required": [
          "id",
          "span",
          "searched"
        ],
        "title": "Claim",
        "description": "Claim fields in the current API contract."
      },
      "DocumentTrack": {
        "properties": {
          "source_ids": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Source Ids",
            "description": "Ordered source IDs for this document. Resolve each ID in the top-level sources map."
          },
          "scores": {
            "additionalProperties": {
              "type": "number"
            },
            "type": "object",
            "title": "Scores",
            "description": "Rank scores keyed by source ID for this document context. Larger values rank higher; these are not confidence percentages."
          },
          "search_queries": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Search Queries",
            "description": "The search phrases sent to the academic providers for this document, most relevant first. They are written from the submitted text by a language model, two for most texts and three past 3000 characters, and reused for later pages. Empty until the track has run."
          },
          "page": {
            "type": "integer",
            "title": "Page",
            "description": "Latest page fetched for this track, initially 1. A page is one provider results page of up to 10 sources for every search query, in query order, so a document page holds up to 20 sources, or 30 for texts over 3000 characters. Additional-page endpoints are outside the initial public surface.",
            "default": 1
          },
          "has_more": {
            "type": "boolean",
            "title": "Has More",
            "description": "True when at least one search query filled its latest provider page, so another page may exist. It is not a guarantee or an initial-release pagination capability.",
            "default": false
          },
          "status": {
            "$ref": "#/components/schemas/TrackStatus",
            "default": "pending",
            "description": "State of this result track, distinct from overall job status."
          }
        },
        "type": "object",
        "title": "DocumentTrack",
        "description": "DocumentTrack fields in the current API contract."
      },
      "Enrichment": {
        "properties": {
          "level": {
            "$ref": "#/components/schemas/EnrichmentLevel",
            "description": "Effective enrichment level used for this source."
          },
          "relevance_rating": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/RelevanceRating"
              },
              {
                "type": "null"
              }
            ],
            "description": "Qualitative relevance label. These current wire values are case-sensitive, human-readable strings; see the enum reference."
          },
          "analysis": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Analysis",
            "description": "Markdown explanation of the source’s relevance. Null at low enrichment."
          },
          "evidence": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/SupportingEvidence"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Evidence",
            "description": "Supporting excerpts or findings and their explanations. Null at low; current fallbacks may produce fewer items than the intended 3–5."
          },
          "paper_type": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PaperTypeClassification"
              },
              {
                "type": "null"
              }
            ],
            "description": "Eight classification flags, or null at low enrichment. Heuristic/model output is not independent verification."
          }
        },
        "type": "object",
        "required": [
          "level"
        ],
        "title": "Enrichment",
        "description": "Enrichment fields in the current API contract."
      },
      "EnrichmentLevel": {
        "type": "string",
        "enum": [
          "none",
          "low",
          "medium",
          "high"
        ],
        "title": "EnrichmentLevel",
        "description": "EnrichmentLevel values in the current API contract."
      },
      "ErrorDetail": {
        "properties": {
          "code": {
            "type": "string",
            "title": "Code",
            "description": "Stable machine-readable error identifier."
          },
          "message": {
            "type": "string",
            "title": "Message",
            "description": "Human-readable description. Clients should branch on code, not this text."
          },
          "details": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Details",
            "description": "Optional additional error details; structure depends on the error."
          }
        },
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "title": "ErrorDetail",
        "description": "ErrorDetail fields in the current API contract."
      },
      "ErrorResponse": {
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ErrorDetail",
            "description": "Structured API error details."
          }
        },
        "type": "object",
        "required": [
          "error"
        ],
        "title": "ErrorResponse",
        "description": "ErrorResponse fields in the current API contract."
      },
      "Filters": {
        "properties": {
          "year_from": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Year From",
            "description": "Inclusive lower publication-year bound. For example, 2020 keeps papers from 2020 onward. Papers without a known year are excluded when this is set. Omit or use null for no lower bound; use year_from <= year_to when both are set."
          },
          "year_to": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Year To",
            "description": "Inclusive upper publication-year bound. For example, 2024 keeps papers published in 2024 or earlier. Papers without a known year are excluded when this is set. Omit or use null for no upper bound. The current API does not validate year-range ordering."
          },
          "min_citations": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "null"
              }
            ],
            "title": "Min Citations",
            "description": "Minimum citation count, inclusive, as an integer >= 0. For example, 10 keeps sources with at least 10 citations. An unknown citation count is treated as zero for filtering, so it passes a threshold of 0 but not a positive threshold. Omit or use null to disable this filter."
          },
          "open_access": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Open Access",
            "description": "When true, require a non-empty pdf_url in the available source metadata. This is a PDF-availability check, not verification of licensing or download access. false, null or omission disables the filter; false does not mean closed-access only."
          },
          "paper_types": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/PaperTypeFilter"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Paper Types",
            "description": "Optional array of exact enum values: research and review. [\"review\"] keeps sources identified as reviews by a metadata-text heuristic; [\"research\"] excludes those sources. Both values disable this local distinction, but upstream providers can still favor reviews when review is included. Omit, use null or [] to avoid a paper-type restriction. This filter is not a peer-review guarantee."
          },
          "venue": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Venue",
            "description": "Optional journal or publication-venue name, such as Nature. Forwarded to Semantic Scholar and Scholar URL-based providers; the current Serper adapter does not forward it. No exact venue check is applied after retrieval, so matching depends on the provider and unrelated venues can appear. Omit, use null or an empty string to disable the venue hint."
          },
          "include_keywords": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Include Keywords",
            "description": "Optional array of strings that must all occur in the combined title, abstract and venue metadata. Local matching uses case-insensitive substrings, not whole words or full-paper text. For example, [\"sleep\", \"memory\"] requires both. Scholar providers also receive each entry as a required quoted phrase added to the query, which can narrow retrieval further. Omit, use null or [] to disable. Entries are trimmed and empty ones ignored."
          },
          "exclude_keywords": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Exclude Keywords",
            "description": "Optional array of strings: exclude a source if any occurs in its title, abstract or venue metadata. Local matching uses case-insensitive substrings. For example, [\"mouse\", \"rat\"] excludes either match, including rat inside another word. Omit, use null or [] to disable. Do not include an empty string, because it matches every source. This does not inspect full-paper text."
          },
          "include_domains": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Include Domains",
            "description": "Optional array of host names. Only sources whose url or pdf_url is on one of these domains, or a subdomain, are returned. For example, [\"ncbi.nlm.nih.gov\"] also matches pubmed.ncbi.nlm.nih.gov. Entries may be given as bare hosts or full URLs; scheme, www. and paths are ignored. Scholar providers receive matching site: operators as well. Sources without a URL are excluded when this is set. Omit, use null or [] to disable."
          },
          "exclude_domains": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Exclude Domains",
            "description": "Optional array of host names. Any source whose url or pdf_url is on one of these domains, or a subdomain, is dropped. For example, [\"wikipedia.org\"] removes en.wikipedia.org results. Entries are normalized like include_domains. Scholar providers receive matching -site: operators as well. Omit, use null or [] to disable."
          },
          "preferred_domains": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Preferred Domains",
            "description": "Optional array of host names, most preferred first. Sources on these domains, or their subdomains, are moved ahead of other results; earlier entries rank ahead of later ones, and provider order is kept within each group. Other domains still appear. Omit, use null or [] to keep the provider ranking only."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "Filters",
        "description": "Filters fields in the current API contract."
      },
      "Granularity": {
        "type": "string",
        "enum": [
          "document",
          "claim"
        ],
        "title": "Granularity",
        "description": "Granularity values in the current API contract."
      },
      "HTTPValidationError": {
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ValidationError"
            },
            "type": "array",
            "title": "Detail",
            "description": "Field-level validation issues returned by FastAPI; differs from the custom error envelope."
          }
        },
        "type": "object",
        "title": "HTTPValidationError",
        "description": "HTTPValidationError fields in the current API contract."
      },
      "JobAccepted": {
        "properties": {
          "job_id": {
            "type": "string",
            "title": "Job Id",
            "description": "Opaque search identifier. Save it and pass it unchanged to GET /search/{job_id}."
          },
          "status": {
            "$ref": "#/components/schemas/JobStatus",
            "default": "queued",
            "description": "Search state at acceptance. POST /search returns queued; GET /search/{job_id} provides the latest state."
          },
          "usage": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Usage"
              },
              {
                "type": "null"
              }
            ],
            "description": "USD quote for this search. estimated_cost is the maximum charge for the requested options. actual_cost is null until the search finishes. balance_after stays null; the dashboard holds the balance."
          }
        },
        "type": "object",
        "required": [
          "job_id"
        ],
        "title": "JobAccepted",
        "description": "JobAccepted fields in the current API contract."
      },
      "JobStatus": {
        "type": "string",
        "enum": [
          "queued",
          "running",
          "partial",
          "completed",
          "failed"
        ],
        "title": "JobStatus",
        "description": "JobStatus values in the current API contract."
      },
      "PaperTypeClassification": {
        "properties": {
          "peer_reviewed": {
            "type": "boolean",
            "title": "Peer Reviewed",
            "description": "Whether analysis classifies the source as peer reviewed."
          },
          "renowned_author": {
            "type": "boolean",
            "title": "Renowned Author",
            "description": "Whether analysis classifies the author as established in the field."
          },
          "top_tier_venue": {
            "type": "boolean",
            "title": "Top Tier Venue",
            "description": "Whether analysis classifies the venue as top tier."
          },
          "meta_analysis": {
            "type": "boolean",
            "title": "Meta Analysis",
            "description": "Whether the source is classified as a meta-analysis."
          },
          "research_article": {
            "type": "boolean",
            "title": "Research Article",
            "description": "Whether the source is classified as original research."
          },
          "review_article": {
            "type": "boolean",
            "title": "Review Article",
            "description": "Whether the source is classified as a literature review."
          },
          "preprint": {
            "type": "boolean",
            "title": "Preprint",
            "description": "Whether the source is classified as a preprint."
          },
          "randomized_control_trial": {
            "type": "boolean",
            "title": "Randomized Control Trial",
            "description": "Whether the source is classified as a randomized controlled trial."
          }
        },
        "type": "object",
        "required": [
          "peer_reviewed",
          "renowned_author",
          "top_tier_venue",
          "meta_analysis",
          "research_article",
          "review_article",
          "preprint",
          "randomized_control_trial"
        ],
        "title": "PaperTypeClassification",
        "description": "PaperTypeClassification fields in the current API contract."
      },
      "PaperTypeFilter": {
        "type": "string",
        "enum": [
          "research",
          "review"
        ],
        "title": "PaperTypeFilter",
        "description": "PaperTypeFilter values in the current API contract."
      },
      "RelevanceRating": {
        "type": "string",
        "enum": [
          "Perfectly Relevant",
          "Relevant",
          "Somewhat Relevant",
          "Not Relevant"
        ],
        "title": "RelevanceRating",
        "description": "RelevanceRating values in the current API contract."
      },
      "SearchMetadata": {
        "properties": {
          "requested_max_claims": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Requested Max Claims",
            "description": "Requested cap for claim searches."
          },
          "actual_max_claims": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Actual Max Claims",
            "description": "Reported number of claims eligible under the request cap and extracted claim count."
          }
        },
        "type": "object",
        "title": "SearchMetadata",
        "description": "SearchMetadata fields in the current API contract."
      },
      "SearchMode": {
        "type": "string",
        "enum": [
          "quick",
          "deep"
        ],
        "title": "SearchMode",
        "description": "SearchMode values in the current API contract."
      },
      "SearchRequest": {
        "properties": {
          "text": {
            "type": "string",
            "minLength": 1,
            "title": "Text",
            "description": "The text to find academic sources for. At least one character; there is no maximum length. Whitespace-only input is currently accepted by validation."
          },
          "mode": {
            "$ref": "#/components/schemas/SearchMode",
            "description": "Search depth. quick fetches one Google Scholar page per generated keyword, falling back to Semantic Scholar, and keeps the provider order. deep fetches Semantic Scholar plus three Scholar pages per keyword, has a language model shortlist the 15 most promising sources, reads each one, and ranks them again with the analysis in hand. deep takes minutes rather than seconds.",
            "default": "quick"
          },
          "enrichment": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/EnrichmentLevel"
              },
              {
                "type": "null"
              }
            ],
            "description": "Analysis level, independent of search depth. Omitted or null means medium for quick and high for deep."
          },
          "granularity": {
            "items": {
              "$ref": "#/components/schemas/Granularity"
            },
            "type": "array",
            "title": "Granularity",
            "description": "Result tracks to produce. Defaults to [\"document\"]. Use [\"claim\"] or [\"document\", \"claim\"] for claim-level results. An empty list currently falls back to document."
          },
          "max_claims": {
            "type": "integer",
            "maximum": 12,
            "minimum": 1,
            "title": "Max Claims",
            "description": "Maximum identified claims to search, from 1 to 12. Defaults to 4. Only applies when the claim track is requested.",
            "default": 4
          },
          "citation_formats": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/CitationFormat"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Citation Formats",
            "description": "Citation formats to populate. Omitted or null selects all five supported formats. An empty array disables citations."
          },
          "filters": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Filters"
              },
              {
                "type": "null"
              }
            ],
            "description": "Optional constraints on source selection. Omit or use null for no additional filtering."
          },
          "webhook_url": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2083,
                "minLength": 1,
                "format": "uri"
              },
              {
                "type": "null"
              }
            ],
            "title": "Webhook Url",
            "description": "Optional http(s) URL. When set, the API POSTs the finished search here. The host must be public unless the deployment allows private URLs. Polling still works."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "text"
        ],
        "title": "SearchRequest",
        "description": "SearchRequest fields in the current API contract.",
        "example": {
          "text": "How do urban green spaces affect mental health?",
          "mode": "quick",
          "enrichment": "none",
          "granularity": [
            "document"
          ],
          "citation_formats": []
        }
      },
      "SearchResult": {
        "properties": {
          "job_id": {
            "type": "string",
            "title": "Job Id",
            "description": "The same opaque search identifier returned by creation."
          },
          "status": {
            "$ref": "#/components/schemas/JobStatus",
            "description": "Overall job state. Inspect this even when the HTTP response is 200."
          },
          "result_type": {
            "type": "string",
            "title": "Result Type",
            "description": "Response discriminator; currently \"search\".",
            "default": "search"
          },
          "document": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DocumentTrack"
              },
              {
                "type": "null"
              }
            ],
            "description": "Document-level result track, or null when not requested or not available."
          },
          "claims": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/Claim"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Claims",
            "description": "Identified claims and their results, or null when the claim track was not requested or is unavailable."
          },
          "sources": {
            "additionalProperties": {
              "$ref": "#/components/schemas/Source"
            },
            "type": "object",
            "title": "Sources",
            "description": "Source objects indexed by source ID. Resolve each document.source_ids or claim.source_ids entry in this map."
          },
          "usage": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Usage"
              },
              {
                "type": "null"
              }
            ],
            "description": "USD quote and charge for this search. estimated_cost is set when the search is created. actual_cost is null while it is running, the calculated charge when it completes, and zero when it fails. balance_after stays null; the dashboard holds the balance."
          },
          "metadata": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/SearchMetadata"
              },
              {
                "type": "null"
              }
            ],
            "description": "Requested and actual claim-search counts, when available."
          }
        },
        "type": "object",
        "required": [
          "job_id",
          "status"
        ],
        "title": "SearchResult",
        "description": "SearchResult fields in the current API contract."
      },
      "Source": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id",
            "description": "Source identifier matching its key in the sources map."
          },
          "title": {
            "type": "string",
            "title": "Title",
            "description": "Title of the academic source."
          },
          "authors": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Authors",
            "description": "Author names in provider order. An empty array means unavailable."
          },
          "year": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Year",
            "description": "Publication year, or null when unknown."
          },
          "venue": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Venue",
            "description": "Publication venue or journal, or null when unknown."
          },
          "abstract": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Abstract",
            "description": "Available abstract or provider summary; may be null."
          },
          "url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Url",
            "description": "Source landing-page URL as a string, or null. Current validation does not enforce a URL format."
          },
          "pdf_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Pdf Url",
            "description": "Available PDF URL as a string, or null."
          },
          "citation_count": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Citation Count",
            "description": "Known citation count, or null when unavailable. Zero differs from an unknown count."
          },
          "score": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Score",
            "description": "Rank score attached to the source. Use the track-specific scores map when a source is shared between tracks."
          },
          "citations": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Citations"
              },
              {
                "type": "null"
              }
            ],
            "description": "Formatted citations. Null when citation_formats is empty; unrequested individual formats are null."
          },
          "enrichment": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Enrichment"
              },
              {
                "type": "null"
              }
            ],
            "description": "Analysis object, or null for enrichment none. Low only populates relevance; other levels may use fallback analysis."
          },
          "parsed_source": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Parsed Source",
            "description": "Text obtained during high enrichment, with abstract fallback when full text is unavailable. Not a guarantee of a complete paper."
          }
        },
        "type": "object",
        "required": [
          "id",
          "title"
        ],
        "title": "Source",
        "description": "Source fields in the current API contract."
      },
      "SupportingEvidence": {
        "properties": {
          "quote": {
            "type": "string",
            "title": "Quote",
            "description": "An excerpt or finding attributed to the source. Check against the original source before quoting it as verbatim."
          },
          "explanation": {
            "type": "string",
            "title": "Explanation",
            "description": "Markdown explanation connecting the evidence to the research text."
          },
          "tag": {
            "type": "string",
            "title": "Tag",
            "description": "Short descriptive label for the evidence."
          }
        },
        "type": "object",
        "required": [
          "quote",
          "explanation",
          "tag"
        ],
        "title": "SupportingEvidence",
        "description": "SupportingEvidence fields in the current API contract."
      },
      "TrackStatus": {
        "type": "string",
        "enum": [
          "pending",
          "running",
          "done",
          "failed"
        ],
        "title": "TrackStatus",
        "description": "TrackStatus values in the current API contract."
      },
      "Usage": {
        "properties": {
          "currency": {
            "type": "string",
            "title": "Currency",
            "default": "USD",
            "description": "Currency for the amounts in this object. Currently USD."
          },
          "estimated_cost": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Estimated Cost",
            "description": "Maximum USD charge for the requested mode, enrichment, source count, and claim count. Computed before the search runs. Null only on older records."
          },
          "actual_cost": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Actual Cost",
            "description": "USD reported for this search. Null while the search is still running. Zero when the search fails. On success, retrieval plus per-source enrichment for the sources actually returned, which can be below the estimate. A finished track with no sources still includes its retrieval fee."
          },
          "discount_percent": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Discount Percent",
            "description": "Subscription discount already applied to estimated_cost and actual_cost, as a whole percentage: 15 for Pro, 35 for Ultra, 50 for Max, 0 without a subscription. Null only on older records."
          },
          "balance_after": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Balance After",
            "description": "Prepaid balance after this search. Null on this response. Create reads the dashboard balance and rejects with 402 when it cannot cover the estimate. The later usage report is what decreases it."
          }
        },
        "type": "object",
        "title": "Usage",
        "description": "Usage fields in the current API contract."
      },
      "ValidationError": {
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "integer"
                }
              ]
            },
            "type": "array",
            "title": "Location",
            "description": "Location of the invalid value, such as [\"body\", \"max_claims\"]."
          },
          "msg": {
            "type": "string",
            "title": "Message",
            "description": "Human-readable validation explanation."
          },
          "type": {
            "type": "string",
            "title": "Error Type",
            "description": "Machine-readable validation issue type."
          },
          "input": {
            "title": "Input",
            "description": "Invalid input value when included by FastAPI. Avoid logging validation payloads containing sensitive text."
          },
          "ctx": {
            "type": "object",
            "title": "Context",
            "description": "Additional validation context, when present."
          }
        },
        "type": "object",
        "required": [
          "loc",
          "msg",
          "type"
        ],
        "title": "ValidationError",
        "description": "ValidationError fields in the current API contract."
      }
    },
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "description": "Dashboard API key, sent as Bearer srcly_…",
        "scheme": "bearer"
      }
    }
  }
}
