# Generated from dataville-api — do not edit by hand.
# Source: backend/src/docs/openapi.yaml
openapi: 3.0.3
info:
  title: 'Dataville API'
  version: 1.0.0
  description: "Dataville is a unified data API: a single REST surface over Wikipedia, arXiv,\nProject Gutenberg, the US Census, USDA FoodData, Papers with Code, SEC EDGAR,\nOpenAlex, PyPI and Stack Exchange — plus bulk CSV/Parquet/DuckDB export, a\nSQL endpoint, and \"bring your own data\" Google Sheets connections.\n\n## Authentication\n\nData, export and query endpoints never hard-reject at the middleware level — an\nunauthenticated caller simply gets the anonymous tier (50 requests per day per\nIP).\n\n### API key\n\n```\nAuthorization: Bearer dataville_xxxxxxxxxxxxxxxx\n```\n\nCreate and manage keys from the **API Keys** page in the\n[Dataville dashboard](https://app.dataville.com). Keys start with `dataville_`\n(legacy keys start with `datax_`). For clients that cannot set headers — DuckDB\nWASM, the DuckDB CLI's `httpfs` reader — the same key may be passed as the\n`?token=` query parameter.\n\nThe Dataville web app authenticates with its own session tokens; those routes\nare internal and not part of the public API surface.\n\n## Credits and metering\n\nAuthenticated usage is metered against a monthly **Dataville credit** balance in\nUSD, shared across an account's sessions and API keys. Each tier is granted\ncredit every month — Free **$5**, Developer **$29**, Pro **$99** — and each\nquery deducts a per-source, per-tier amount:\n\n| Class | Free | Developer | Pro |\n| --- | --- | --- | --- |\n| Open/public datasets | $0.0050 | $0.0050 | $0.0040 |\n| Wikipedia (premium) | $0.0250 | $0.0150 | $0.0125 |\n\nA `POST /api/v1/query` call is charged per row returned. When the balance\nreaches zero the API returns **402 Payment Required** until the monthly reset,\na plan upgrade, or a credit top-off. Every metered response carries:\n\n- `X-Credits-Remaining` — remaining balance, USD\n- `X-Credit-Cost` — amount charged for this request, USD\n\nCheck your request quota any time via `GET /api/v1/auth/usage`; your credit\nbalance is reported on every metered response in `X-Credits-Remaining` and on\nthe dashboard's **Billing** page.\n\n## Rate limiting\n\nAnonymous callers are limited by IP (50 requests per 24 hours) and receive\n**429** when they exceed it, with `X-RateLimit-Limit`, `X-RateLimit-Remaining`\nand `X-RateLimit-Reset` headers. Authenticated callers are governed by credits\nrather than a request count. Export and SQL routes carry their own stricter\nlimit (1,000 per day per account), as does support submission (5 per hour).\n\n## Errors\n\nEvery error body carries a human-readable `error`/`message` string that is safe\nto show a user verbatim, and data-source errors additionally carry a stable\n`data.code` (`UPSTREAM_RATE_LIMITED`, `UPSTREAM_UNAVAILABLE`,\n`UPSTREAM_FORBIDDEN`, `UPSTREAM_BAD_RESPONSE`, `SOURCE_UNAVAILABLE`,\n`INTERNAL_ERROR`) that clients should branch on instead of parsing prose. When\nan upstream tells us how long to wait, the response sets `Retry-After`.\n\n## Stale results\n\nA live source that is down is answered from Dataville's own stored copy where\none exists. Those responses include a `data.stale` object naming the source and\nwhen the copy was taken — the answer is real, but may be out of date.\n"
  contact:
    name: 'Dataville Support'
    email: support@dataville.com
  license:
    name: MIT
    url: 'https://opensource.org/licenses/MIT'
servers:
  -
    url: 'https://api.dataville.com'
    description: Production
tags:
  -
    name: 'Data sources'
    description: 'Keyword search and article lookup across every supported dataset.'
  -
    name: 'Export and SQL'
    description: 'Bulk CSV/Parquet/DuckDB download and SELECT-only SQL over the datasets.'
  -
    name: 'Connected sheets'
    description: 'Connect a Google Sheet and query it alongside the public datasets.'
  -
    name: Usage
    description: "Usage and quota for your API key (or your IP when anonymous). Keys\nare created and revoked from the dashboard's **API Keys** page."
security:
  -
    ApiKeyAuth: []
paths:
  '/{source}/{keywords}':
    get:
      tags:
        - 'Data sources'
      operationId: searchSource
      summary: 'Search a data source'
      description: "Search any supported data source by keyword and return the single most\nrelevant result. This is the primary endpoint of the API.\n\nSupported `source` values (aliases in parentheses): `wiki` (`wikipedia`),\n`census`, `arxiv`, `gutenberg`, `fooddata`, `paperswithcode`, `edgar`\n(`sec`, `sec-edgar`), `news` (`frontpage-news`), `stackexchange`\n(`stackoverflow`, `stack-exchange`), `pypi`, `openalex`.\n\n`keywords` is free text and may contain spaces and slashes; URL-encode it.\n\n```bash\ncurl -H \"Authorization: Bearer $DATAVILLE_API_KEY\" \\\n  \"https://api.dataville.com/arxiv/attention%20is%20all%20you%20need\"\n```\n"
      parameters:
        -
          $ref: '#/components/parameters/Source'
        -
          $ref: '#/components/parameters/Keywords'
        -
          $ref: '#/components/parameters/Summary'
        -
          $ref: '#/components/parameters/TokenQuery'
      responses:
        '200':
          description: 'The most relevant result for the query.'
          headers:
            X-Credits-Remaining:
              $ref: '#/components/headers/XCreditsRemaining'
            X-Credit-Cost:
              $ref: '#/components/headers/XCreditCost'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataSuccessResponse'
              examples:
                arxiv:
                  summary: 'An arXiv paper'
                  value:
                    status: success
                    account_state: authenticated
                    data:
                      id: '2301.00001'
                      title: 'Attention Is All You Need'
                      body: "Authors: Ashish Vaswani, Noam Shazeer\nPublished: 2017-06-12\nSummary: The dominant sequence transduction models..."
                      source: arxiv
                      language: en
                      last_updated: '2017-06-12T00:00:00Z'
                      entities:
                        - transformer
                        - attention
                        - 'machine translation'
                      metadata:
                        arxiv_id: '1706.03762'
                        categories:
                          - cs.CL
                          - cs.LG
                        pdf_url: 'https://arxiv.org/pdf/1706.03762'
                    usage:
                      requests_remaining: 4821
                      request_limit: 5000
                      credits_total: 29
                      credits_remaining: 24.11
                      credits_used: 4.89
                      request_cost: 0.005
                stale:
                  summary: 'Served from Dataville''s stored copy while the upstream is down'
                  value:
                    status: success
                    account_state: authenticated
                    data:
                      id: '2301.00001'
                      title: 'Attention Is All You Need'
                      body: 'Authors: Ashish Vaswani…'
                      source: arxiv
                      language: en
                      last_updated: '2017-06-12T00:00:00Z'
                      entities: []
                      metadata:
                        served_from_cache: true
                        cached_at: '2026-08-24T09:12:00Z'
                      stale:
                        message: 'arxiv is unavailable right now, so this result comes from our stored copy and may be out of date or incomplete. Nothing is wrong with your query.'
                        cached_at: '2026-08-24T09:12:00Z'
                    usage:
                      requests_remaining: 49
                      request_limit: 50
        '400':
          description: 'Unsupported source, or missing source/keywords.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataErrorResponse'
              example:
                status: error
                account_state: anonymous
                data:
                  error: 'Unsupported data source: "foo" is not one we offer, so nothing was searched and you were not charged. Use one of these source ids instead: wiki, wikipedia, census, arxiv, gutenberg, fooddata, paperswithcode, edgar, sec, sec-edgar, news, frontpage-news, stackexchange, stackoverflow, stack-exchange, pypi, openalex.'
                  code: UNSUPPORTED_SOURCE
        '402':
          $ref: '#/components/responses/InsufficientCredit'
        '404':
          description: 'No results found for the query in this source.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataErrorResponse'
              example:
                status: error
                account_state: anonymous
                data:
                  error: 'No results found for "zzzzzz" in arxiv. Nothing went wrong and you were not charged — try different or broader keywords, or another source.'
                  code: NOT_FOUND
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/DataInternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  '/https:/{wikipediaUrl}':
    get:
      tags:
        - 'Data sources'
      operationId: fetchByUrl
      summary: 'Fetch an article by pasting its URL'
      description: "Paste a Wikipedia URL directly onto the API host and get the article back.\nThe path is the URL with its double slash collapsed to one, so\n`https://en.wikipedia.org/wiki/Moon_landing` becomes:\n\n```bash\ncurl \"https://api.dataville.com/https:/en.wikipedia.org/wiki/Moon_landing\"\n```\n\n`http:/` works the same way. Metered at the premium (Wikipedia) rate.\n"
      parameters:
        -
          name: wikipediaUrl
          in: path
          required: true
          description: 'The rest of the article URL, e.g. `en.wikipedia.org/wiki/Moon_landing`.'
          schema:
            type: string
          example: en.wikipedia.org/wiki/Moon_landing
        -
          $ref: '#/components/parameters/Summary'
        -
          $ref: '#/components/parameters/TokenQuery'
      responses:
        '200':
          description: 'The article at that URL.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataSuccessResponse'
        '400':
          description: 'The URL contained no recognisable article title (`INVALID_URL`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataErrorResponse'
        '402':
          $ref: '#/components/responses/InsufficientCredit'
        '404':
          description: 'No article matched the URL''s title (`NOT_FOUND`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataErrorResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/DataInternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /api/v1/export:
    get:
      tags:
        - 'Export and SQL'
      operationId: listExports
      summary: 'List available exports'
      description: 'Catalogue of every exportable source and its download URLs. No auth required.'
      security: []
      responses:
        '200':
          description: 'The export catalogue.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  exports:
                    type: array
                    items:
                      type: object
                      properties:
                        source:
                          type: string
                          example: gutenberg
                        csv:
                          type: string
                          example: '/api/v1/export/gutenberg?format=csv'
                        parquet:
                          type: string
                          example: '/api/v1/export/gutenberg?format=parquet'
  '/api/v1/export/{source}':
    get:
      tags:
        - 'Export and SQL'
      operationId: exportSource
      summary: 'Download one source as CSV or Parquet'
      description: "Stream a whole dataset as CSV or Parquet. Requires authentication and is\ncredit-metered; a failed or aborted download is refunded.\n\n```bash\ncurl -L -H \"Authorization: Bearer $DATAVILLE_API_KEY\" \\\n  \"https://api.dataville.com/api/v1/export/gutenberg?format=parquet\" \\\n  -o gutenberg.parquet\n```\n\nParquet responses support HTTP range requests so DuckDB's `httpfs` can read\nthem remotely:\n\n```sql\nSELECT * FROM read_parquet('https://api.dataville.com/api/v1/export/gutenberg?format=parquet&token=YOUR_KEY') LIMIT 10;\n```\n"
      parameters:
        -
          name: source
          in: path
          required: true
          description: 'One of the exportable sources.'
          schema:
            type: string
            enum:
              - gutenberg
              - fooddata
              - pwc_methods
              - pwc_datasets
              - wiki
              - edgar
              - stackexchange
              - pypi
              - openalex
              - arxiv
        -
          name: format
          in: query
          required: false
          schema:
            type: string
            enum:
              - csv
              - parquet
              - db
            default: csv
        -
          $ref: '#/components/parameters/TokenQuery'
      responses:
        '200':
          description: "The dataset file.\n\n<h4>Response headers</h4>\n\n| Header | Example |\n| --- | --- |\n| `Content-Disposition` | `attachment; filename=\"gutenberg.parquet\"` |\n| `X-Dataville-Success` | `true` |\n| `X-Dataville-Message` | `gutenberg.parquet downloaded successfully.` |\n| `RateLimit-Remaining` | `999` |\n\n`format=parquet` adds `Content-Length`, `Accept-Ranges: bytes`,\n`ETag` and `Last-Modified`; a range request against it returns `206`\nwith `Content-Range`, which is what lets DuckDB read the file\nremotely. `format=db` sets `Content-Length`. `format=csv` is\nstreamed as it is generated and sends neither.\n"
          headers:
            X-Dataville-Success:
              schema:
                type: string
                example: 'true'
              description: 'Set to `true` when the file was generated and streamed successfully.'
            X-Dataville-Message:
              schema:
                type: string
                example: 'gutenberg.csv downloaded successfully.'
              description: 'Human-readable confirmation of the download.'
          content:
            text/csv:
              schema:
                type: string
                format: binary
                example: 'Binary file stream — gutenberg.csv or gutenberg.parquet'
            application/octet-stream:
              schema:
                type: string
                format: binary
        '400':
          description: 'Unsupported source or format.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleError'
        '401':
          $ref: '#/components/responses/ExportUnauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredit'
        '429':
          $ref: '#/components/responses/ExportRateLimited'
  /api/v1/export/dataville.db:
    get:
      tags:
        - 'Export and SQL'
      operationId: exportDuckDbFile
      summary: 'Download every dataset as a DuckDB database'
      description: 'A single `.db` file containing a table per source, ready to open in DuckDB.'
      parameters:
        -
          $ref: '#/components/parameters/TokenQuery'
      responses:
        '200':
          description: "The DuckDB database file.\n\n<h4>Response headers</h4>\n\n| Header | Example |\n| --- | --- |\n| `Content-Disposition` | `attachment; filename=\"dataville.db\"` |\n| `Content-Length` | `166735872` |\n| `X-Dataville-Success` | `true` |\n| `X-Dataville-Message` | `DuckDB database file downloaded successfully.` |\n| `RateLimit-Remaining` | `999` |\n"
          headers:
            X-Dataville-Success:
              schema:
                type: string
                example: 'true'
              description: 'Set to `true` when the file was generated and streamed successfully.'
            X-Dataville-Message:
              schema:
                type: string
                example: 'DuckDB database file downloaded successfully.'
              description: 'Human-readable confirmation of the download.'
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
                example: 'Binary file stream — dataville.db'
        '401':
          $ref: '#/components/responses/ExportUnauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredit'
        '429':
          $ref: '#/components/responses/ExportRateLimited'
  /api/v1/export/dataville.csv.zip:
    get:
      tags:
        - 'Export and SQL'
      operationId: exportAllCsvZip
      summary: 'Download every dataset as a zip of CSVs'
      parameters:
        -
          $ref: '#/components/parameters/TokenQuery'
      responses:
        '200':
          description: "Zip archive, one CSV per source.\n\n<h4>Response headers</h4>\n\n| Header | Example |\n| --- | --- |\n| `Content-Disposition` | `attachment; filename=\"dataville-csv.zip\"` |\n| `X-Dataville-Success` | `true` |\n| `X-Dataville-Message` | `dataville-csv.zip downloaded successfully.` |\n| `RateLimit-Remaining` | `999` |\n\nThe archive is streamed as it is built, so no `Content-Length` is\nsent.\n"
          headers:
            X-Dataville-Success:
              schema:
                type: string
                example: 'true'
              description: 'Set to `true` when the archive was generated and streamed successfully.'
            X-Dataville-Message:
              schema:
                type: string
                example: 'dataville-csv.zip downloaded successfully.'
              description: 'Human-readable confirmation of the download.'
          content:
            application/zip:
              schema:
                type: string
                format: binary
                example: 'Binary file stream — dataville.csv.zip'
        '401':
          $ref: '#/components/responses/ExportUnauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredit'
        '429':
          $ref: '#/components/responses/ExportRateLimited'
  /api/v1/export/dataville.parquet.zip:
    get:
      tags:
        - 'Export and SQL'
      operationId: exportAllParquetZip
      summary: 'Download every dataset as a zip of Parquet files'
      parameters:
        -
          $ref: '#/components/parameters/TokenQuery'
      responses:
        '200':
          description: "Zip archive, one Parquet file per source.\n\n<h4>Response headers</h4>\n\n| Header | Example |\n| --- | --- |\n| `Content-Disposition` | `attachment; filename=\"dataville-parquet.zip\"` |\n| `X-Dataville-Success` | `true` |\n| `X-Dataville-Message` | `dataville-parquet.zip downloaded successfully.` |\n| `RateLimit-Remaining` | `999` |\n\nThe archive is streamed as it is built, so no `Content-Length` is\nsent.\n"
          headers:
            X-Dataville-Success:
              schema:
                type: string
                example: 'true'
              description: 'Set to `true` when the archive was generated and streamed successfully.'
            X-Dataville-Message:
              schema:
                type: string
                example: 'dataville-parquet.zip downloaded successfully.'
              description: 'Human-readable confirmation of the download.'
          content:
            application/zip:
              schema:
                type: string
                format: binary
                example: 'Binary file stream — dataville.parquet.zip'
        '401':
          $ref: '#/components/responses/ExportUnauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredit'
        '429':
          $ref: '#/components/responses/ExportRateLimited'
  /api/v1/query:
    post:
      tags:
        - 'Export and SQL'
      operationId: runSqlQuery
      summary: 'Run a SQL SELECT across the datasets'
      description: "Execute a read-only SQL query over the datasets in DuckDB. Only `SELECT`\nstatements are accepted; anything else is rejected by the validator.\n\n- Table names are the source names (`gutenberg`, `arxiv`, `edgar`, …).\n- Connected Google Sheets can be joined in by passing their ids in\n  `datasetIds` and referencing them by their **alias** (never the internal\n  view name).\n- Queries are capped at 10,000 characters, 1,000 rows and 30 seconds.\n- Charged **per row returned**.\n\n```bash\ncurl -X POST https://api.dataville.com/api/v1/query \\\n  -H \"Authorization: Bearer $DATAVILLE_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"sql\":\"SELECT title, author FROM gutenberg LIMIT 10\"}'\n```\n"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - sql
              properties:
                sql:
                  type: string
                  maxLength: 10000
                  description: 'A single SELECT statement.'
                  example: 'SELECT title, author FROM gutenberg LIMIT 10'
                datasetIds:
                  type: array
                  description: 'Ids of the caller''s connected sheets to make available to the query.'
                  items:
                    type: string
      responses:
        '200':
          description: 'Query results.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  row_count:
                    type: integer
                    example: 10
                  rows:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                  sql_executed:
                    type: string
                  available_tables:
                    type: array
                    items:
                      type: string
                  usage:
                    type: object
                    properties:
                      rows_charged:
                        type: integer
                      request_cost:
                        type: number
                        format: float
                      credits_remaining:
                        type: number
                        format: float
        '400':
          description: 'Missing/oversized SQL, a rejected statement, or a DuckDB error.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleError'
              example:
                status: error
                message: 'Only SELECT statements are allowed.'
        '401':
          $ref: '#/components/responses/ExportUnauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredit'
        '403':
          description: 'A dataset id in `datasetIds` is not owned by the caller.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleError'
        '408':
          description: 'Query exceeded the 30-second limit.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleError'
        '429':
          $ref: '#/components/responses/ExportRateLimited'
  '/api/v1/datasets/{id}/query':
    post:
      tags:
        - 'Connected sheets'
      operationId: queryDataset
      summary: 'Run SQL against one connected sheet'
      description: "A SELECT-only sandbox scoped to a single dataset. Reference the sheet as\n`sheet_data` in the SQL. Accepts an API key as well as a Clerk session, so\nconnected data is queryable programmatically.\n\n```bash\ncurl -X POST https://api.dataville.com/api/v1/datasets/my-budget-2026/query \\\n  -H \"Authorization: Bearer $DATAVILLE_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"sql\":\"SELECT category, SUM(amount) FROM sheet_data GROUP BY 1\"}'\n```\n"
      parameters:
        -
          $ref: '#/components/parameters/DatasetId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - sql
              properties:
                sql:
                  type: string
                  maxLength: 10000
                  example: 'SELECT * FROM sheet_data LIMIT 10'
      responses:
        '200':
          description: 'Query results.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  row_count:
                    type: integer
                  rows:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                  sql_executed:
                    type: string
        '400':
          description: 'Invalid SQL, or the dataset is not in a queryable state.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DatasetNotFound'
        '408':
          description: 'Query exceeded the 30-second limit.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleError'
  '/api/v1/datasets/{id}/search':
    get:
      tags:
        - 'Connected sheets'
      operationId: searchDataset
      summary: 'Keyword-search a connected sheet'
      description: 'Search every column of a connected sheet without writing SQL. Accepts an API key.'
      parameters:
        -
          $ref: '#/components/parameters/DatasetId'
        -
          name: q
          in: query
          required: true
          description: 'Search text (max 500 characters).'
          schema:
            type: string
            maxLength: 500
        -
          name: limit
          in: query
          schema:
            type: integer
            default: 50
      responses:
        '200':
          description: 'Matching rows.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  row_count:
                    type: integer
                  rows:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
        '400':
          description: '`q` is missing or too long, or the dataset is not ready.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/DatasetNotFound'
  /api/v1/auth/usage:
    get:
      tags:
        - Usage
      operationId: getAuthUsage
      summary: 'Get request usage for the caller'
      description: "Request counts and limits for the caller — authenticated by token, or by IP\nwhen no token is sent. Your credit balance is reported on every metered\nresponse in `X-Credits-Remaining` and on the dashboard's **Billing** page.\n"
      responses:
        '200':
          description: 'Usage for the current window.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  account_state:
                    type: string
                    enum:
                      - authenticated
                      - anonymous
                  usage:
                    type: object
                    properties:
                      requests_remaining:
                        type: integer
                      request_limit:
                        type: integer
components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      description: "A Dataville API key, sent as `Authorization: Bearer dataville_...`. Clients\nthat cannot set headers may pass the same key as `?token=`.\n"
  parameters:
    Source:
      name: source
      in: path
      required: true
      description: 'Data source name or alias.'
      schema:
        type: string
        enum:
          - wiki
          - wikipedia
          - census
          - arxiv
          - gutenberg
          - fooddata
          - paperswithcode
          - edgar
          - sec
          - sec-edgar
          - news
          - frontpage-news
          - stackexchange
          - stackoverflow
          - stack-exchange
          - pypi
          - openalex
      example: arxiv
    Keywords:
      name: keywords
      in: path
      required: true
      description: 'URL-encoded search terms. May contain spaces and slashes.'
      schema:
        type: string
      example: attention%20is%20all%20you%20need
    Summary:
      name: summary
      in: query
      required: false
      description: 'Truncate `data.body` to the first 100 characters.'
      schema:
        type: boolean
        default: false
    TokenQuery:
      name: token
      in: query
      required: false
      description: "API key as a query parameter, for clients that cannot set an\n`Authorization` header (DuckDB WASM, the DuckDB CLI). Prefer the header\nwherever possible — query strings end up in logs.\n"
      schema:
        type: string
    DatasetId:
      name: id
      in: path
      required: true
      description: 'The dataset''s UUID or its friendly slug (e.g. `my-budget-2026`).'
      schema:
        type: string
  headers:
    XCreditsRemaining:
      description: 'Remaining credit balance, in USD.'
      schema:
        type: number
        format: float
    XCreditCost:
      description: 'Credit charged for this request, in USD.'
      schema:
        type: number
        format: float
    XRateLimitLimit:
      description: 'Request limit for the current window (anonymous callers).'
      schema:
        type: integer
    XRateLimitRemaining:
      description: 'Requests remaining in the current window.'
      schema:
        type: integer
    XRateLimitReset:
      description: 'When the window resets.'
      schema:
        type: string
    RetryAfter:
      description: 'Seconds to wait before retrying, when known.'
      schema:
        type: integer
  schemas:
    DataSourceResult:
      type: object
      description: 'The common shape every data source returns.'
      properties:
        id:
          type: string
          description: 'Source-specific identifier.'
        title:
          type: string
        body:
          type: string
          description: "Human- and LLM-readable text: newline-delimited `Key: value` lines.\nStructured values live in `metadata`.\n"
        source:
          type: string
          example: arxiv
        language:
          type: string
          example: en
        last_updated:
          type: string
          format: date-time
        entities:
          type: array
          items:
            type: string
        metadata:
          type: object
          additionalProperties: true
          description: 'Structured, source-specific fields.'
        stale:
          $ref: '#/components/schemas/StaleNotice'
        onboarding:
          type: object
          description: 'Present only on a new anonymous session''s first response.'
          properties:
            message:
              type: string
    StaleNotice:
      type: object
      description: "Present when the upstream source was unavailable and the result came from\nDataville's stored copy instead. The answer is real but may be out of date.\n"
      properties:
        message:
          type: string
        cached_at:
          type: string
          format: date-time
          nullable: true
    Usage:
      type: object
      properties:
        requests_remaining:
          type: integer
        request_limit:
          type: integer
        credits_total:
          type: number
          format: float
          description: 'Monthly credit grant in USD. Metered callers only.'
        credits_remaining:
          type: number
          format: float
        credits_used:
          type: number
          format: float
        request_cost:
          type: number
          format: float
          description: 'Credit charged for this request, in USD.'
    DataSuccessResponse:
      type: object
      properties:
        status:
          type: string
          enum:
            - success
        account_state:
          type: string
          enum:
            - authenticated
            - anonymous
        data:
          $ref: '#/components/schemas/DataSourceResult'
        usage:
          $ref: '#/components/schemas/Usage'
    DataErrorResponse:
      type: object
      description: "The data-route error envelope. `data.error` is written for the person who\ntriggered it and is safe to display verbatim; branch on `data.code`.\n"
      properties:
        status:
          type: string
          enum:
            - error
        account_state:
          type: string
          enum:
            - authenticated
            - anonymous
        data:
          type: object
          properties:
            error:
              type: string
            code:
              type: string
              enum:
                - UPSTREAM_RATE_LIMITED
                - UPSTREAM_UNAVAILABLE
                - UPSTREAM_FORBIDDEN
                - UPSTREAM_BAD_RESPONSE
                - SOURCE_UNAVAILABLE
                - INTERNAL_ERROR
                - INVALID_URL
                - INVALID_REQUEST
                - UNSUPPORTED_SOURCE
                - NOT_FOUND
    InsufficientCreditResponse:
      type: object
      properties:
        status:
          type: string
          enum:
            - error
        account_state:
          type: string
          example: authenticated
        error:
          type: string
          example: insufficient_credit
        data:
          type: object
          properties:
            error:
              type: string
            code:
              type: string
              enum:
                - INSUFFICIENT_CREDIT
        credits:
          type: object
          properties:
            remaining_usd:
              type: number
              format: float
            request_cost_usd:
              type: number
              format: float
    SimpleError:
      type: object
      properties:
        status:
          type: string
          enum:
            - error
        message:
          type: string
    AuthError:
      type: object
      properties:
        error:
          type: string
        message:
          type: string
  responses:
    InsufficientCredit:
      description: 'Monthly credit exhausted — upgrade, top off, or wait for the reset.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/InsufficientCreditResponse'
    RateLimited:
      description: 'Anonymous request limit exceeded (50 per 24 hours per IP).'
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/XRateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/XRateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/XRateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/DataErrorResponse'
    ExportRateLimited:
      description: 'Export/query limit exceeded (1,000 per day per account).'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/SimpleError'
          example:
            status: error
            message: 'Export rate limit exceeded. Maximum 1,000 exports per day per account.'
    UpstreamUnavailable:
      description: "The upstream source refused or failed the request, and no stored copy could\nanswer it. Not caused by the caller, and no credit was used.\n"
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/DataErrorResponse'
          example:
            status: error
            account_state: authenticated
            data:
              error: 'arXiv is refusing requests right now, so this search could not be completed. This happens on arXiv''s side when their service is under load — it is not a limit on your account and not caused by anything you did. No credit was used. Try again in about 2 minutes.'
              code: UPSTREAM_RATE_LIMITED
    DataInternalError:
      description: 'Something went wrong on Dataville''s side.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/DataErrorResponse'
    Unauthorized:
      description: 'Authentication required, or the token is invalid.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AuthError'
          example:
            error: Unauthorized
            message: 'No token provided'
    ExportUnauthorized:
      description: 'Authentication required.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/SimpleError'
          example:
            status: error
            message: 'Authentication required. Include your API key in the Authorization header.'
    DatasetNotFound:
      description: 'No such dataset for this account.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/SimpleError'
          example:
            status: error
            message: 'Dataset not found.'
