View legacy API spec Download spec Dashboard
Raw OpenAPI specification (YAML - inlined for the scrapers that need it)
openapi: 3.2.0
info:
  title: Riveter API
  description: |
    ## Overview

    The Riveter API lets you **enrich** data, **build datasets**, **scrape** webpages, quickly **search** the web, and **extract** data from complicated websites programmatically.

    - **An enrichment** takes rows of input data and fills in new columns using AI, web searches, web scrapes, and other tools. For example, given a list of companies, an enrichment can pull their tech stack, analyze their pricing structure, determine what security tools they use, and much more.
    - **A list build** finds rows for a description ("top 50 US SaaS companies") — identifier columns only, which an enrichment then researches. "Build a list" in the app; the [List builds](#tag/list-builds) section here.
    - **A dataset** (beta, per account) is a table in Riveter — input columns for what each row is, output columns an enrichment fills. Start at [POST /datasets](#tag/datasets-beta/post/datasets) in the [Datasets (beta)](#tag/datasets-beta) section at the end of this page.
    - **A scrape** lets you turn a URL into easily parseable text.
    - **A quick_search** lets you quickly web search a query, and pull structured results with urls, titles, and snippets — synchronously, in one request.
    - **A search_agent call** asks one question and gets one AI-researched answer back — the same agent loop that fills a single enrichment cell, without setting up an enrichment.

    Every asynchronous operation returns a **run** with an id like `run_...`, and every run — enrichment, dataset build, extraction, or search — is tracked, fetched, and stopped the same way through `/runs`.

    ## The run lifecycle

    Every kickoff endpoint (`/enrich`, `/datasets`, `/extractions/{id}/runs`, ...) returns `201 Created` with a **run**. From there:

    | Request | What it does |
    |---------|--------------|
    | `GET /runs/{id}` | Check status (`pending` → `enqueued` → `processing` → `success` or `stopped`) |
    | `GET /runs/{id}/result?wait=30` | Fetch the output. `wait` long-polls up to 50 seconds; `output` is `null` until the run finishes |
    | `POST /runs/{id}/stop` | Stop a run early |

    Prefer webhooks over polling: pass `webhook_url` on kickoff and Riveter POSTs the results to you when the run finishes. If you poll, an interval of 10-20 seconds is plenty.

    Every kickoff and every `/runs` endpoint returns the same **run** — see the [Runs](#tag/runs) section for the full shape.

    ## Legacy endpoints

    This is the second generation of the Riveter API. The first generation used verb-style paths (`/run_new_enrichment`, `/run_status`, `/run_data`, `/build_dataset`, ...). Those endpoints are legacy: they keep working at the same paths, but new work should use this API. Every endpoint documented here is current — none are legacy.

    Common legacy → current mappings: `/run_new_enrichment` → `/enrich`, `/run_enrichment` → `/enrich` + `enrichment_id`, `/run_status` → `/runs/{id}`, `/run_data` → `/runs/{id}/result`, `/build_dataset` → `/datasets`, `/monitor_enrichment` → `/monitors`. The full table is in the [legacy API docs](./openapi.legacy.yaml) ("Migrating to the current API").

    Legacy responses say so themselves: the body carries `"legacy": { "notice", "successor", "docs" }` and the response has `Deprecation: true` plus a `Link` header (`rel="successor-version"`). Endpoints documented here never carry them.

    ## Webhooks

    Pass `webhook_url` in the JSON body when starting a run and Riveter POSTs the full results to your URL when it finishes (events: `run.completed`, `run.stopped`, `run.finished`). Dataset builds take `dataset_webhook_url`. Failed deliveries are retried up to 2 times; your endpoint should return a 2xx.

    The webhook payload shape is shared with the legacy API — see the [legacy API docs](./openapi.legacy.yaml) ("Webhooks" section) for the full payload reference.

    ## Authentication

    All endpoints require an API key via the Authorization header:
    ```
    Authorization: Bearer YOUR_API_KEY
    ```
    [Get an API key here](https://app.riveterhq.com/settings/api)

    ## Rate limiting

    Default: 30 requests per minute per endpoint group. Responses include `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers. Row caps: up to 10,000 rows per `/enrich` request with `enrichment_id`, 1,000 rows with an inline config (`output` or `prompt` + `attributes`).

    ## MCP server

    Use Riveter from Claude, ChatGPT, Cursor, or any MCP-compatible AI assistant. Pick one of the two ways to connect.

    ### Hosted server (claude.ai, Claude Desktop, Cowork, mobile, ChatGPT, Claude Code, Cursor)

    Add `https://mcp.riveterhq.com/mcp` as a custom connector, then click Connect. A browser window opens on Riveter: sign in and click **Allow**. That creates an API key for the connection in [Settings → API keys](https://app.riveterhq.com/settings/api); revoke it there to disconnect. Nothing runs on your machine.

    **Claude Code:**
    ```bash
    claude mcp add --transport http riveter https://mcp.riveterhq.com/mcp
    ```
    Then run `/mcp` inside Claude Code and choose Authenticate.

    **Cursor** — paste into your MCP config, then click Connect in the MCP settings:
    ```json
    {
      "mcpServers": {
        "riveter": {
          "url": "https://mcp.riveterhq.com/mcp"
        }
      }
    }
    ```

    If your client cannot open a browser, pass an [API key](https://app.riveterhq.com/settings/api) directly as a request header: `Authorization: Bearer YOUR_API_KEY` (Claude Code: `--header "Authorization: Bearer YOUR_API_KEY"`; Cursor: `"headers": { "Authorization": "Bearer YOUR_API_KEY" }`).

    ### Local server (npx)

    Runs on your machine and needs Node.js and an [API key](https://app.riveterhq.com/settings/api). Use it when your client cannot reach remote servers.

    **Claude Code:**
    ```bash
    claude mcp add riveter \
      --env RIVETER_API_KEY=YOUR_API_KEY \
      -- npx -y --prefer-online riveter-mcp-server@latest
    ```

    **Codex:**
    ```bash
    codex mcp add riveter \
      --env RIVETER_API_KEY=YOUR_API_KEY \
      -- npx -y --prefer-online riveter-mcp-server@latest
    ```

    **Cursor / Windsurf / Claude Desktop** — paste into your MCP config:
    ```json
    {
      "mcpServers": {
        "riveter": {
          "command": "npx",
          "args": ["-y", "--prefer-online", "riveter-mcp-server@latest"],
          "env": {
            "RIVETER_API_KEY": "YOUR_API_KEY"
          }
        }
      }
    }
    ```

    Both servers expose every API endpoint as a tool, with full descriptions and typed parameters. No setup beyond the API key.

    _**Updating:** The hosted server reloads the API definition every 10 minutes on its own. The local (npx) server loads it once when your client starts it, so after an API update restart your AI client to get the new tools. You never need to remove and re-add the server._

    ## Official SDKs

    Prefer a typed client over raw HTTP — the SDKs handle auth, retries (429s and transient GET failures), the `wait` long-poll, polling until a run finishes (`wait_for_result`), and pagination:

    | Language | Install | Package |
    |----------|---------|---------|
    | TypeScript / JavaScript | `npm install riveter-sdk` | [riveter-sdk on npm](https://www.npmjs.com/package/riveter-sdk) |
    | Python | `pip install riveter-sdk` | [riveter-sdk on PyPI](https://pypi.org/project/riveter-sdk/) (import `riveter`) |
    | Ruby | `gem install riveter-sdk` | [riveter-sdk on RubyGems](https://rubygems.org/gems/riveter-sdk) (`require "riveter"`) |
    | Go | `go get github.com/riveterhq/riveter-go` | [riveterhq/riveter-go](https://github.com/riveterhq/riveter-go) |

    Every endpoint on this page shows the equivalent SDK call in all four languages next to the request example.

    ## Timestamps

    All timestamps in responses are ISO 8601 strings with second precision and an explicit UTC offset (e.g. `2026-01-15T12:00:00Z`).

    ## Errors

    Errors use real HTTP status codes and a uniform body:

    ```json
    { "error": { "type": "not_found", "message": "No run found with id run_..." } }
    ```

    Common `type` values: `bad_request`, `not_found`, `forbidden`, `duplicate_run_key`, `insufficient_credits`, `credit_cap_exceeded`, `validation`, `not_implemented`. A dataset endpoint on an account without the datasets beta answers `403` with `feature_not_enabled` (see [POST /datasets](#tag/datasets-beta/post/datasets)).

    One exception: authentication failures (`401`) come from the shared auth layer and use the legacy shape `{ "request_status": "error", "message": "...", "error_type": "unauthorized" }`.

    ## Credits: estimate before you spend

    Every endpoint that spends credits (`/enrich`, `/datasets`, `/datasets/{id}/rows/build`, `/datasets/{id}/extend`, `/enrichments/{id}/datasets`, `/configured_datasets/{id}/build`, `/extractions`, `/extractions/{id}/runs`, `/monitors`, `/quick_search`, `/search_agent`, `/scrape`) accepts two optional fields:

    - `estimate_cost: true` — validate the request and return a [credit estimate](#model/estimatecostresult) without creating or charging anything. The response is `200` with `estimate_cost: true`, the `type` of run that would start, and a `credit_detail` that includes `credits_remaining` and `sufficient_credits`.
    - `max_credits` — a ceiling. When the estimate's `maximum` exceeds it, the request is refused with `422 credit_cap_exceeded` and nothing is charged.

    `estimate_cost` was previously named `dry_run`. The old request field is still accepted, but the response carries `estimate_cost` only; new code should use `estimate_cost`.

    Every run also carries `credit_detail` (`{ estimate: { minimum, maximum, charged_upfront }, credits_used }`) on kickoff and on `GET /runs/{id}`. `maximum` is the contract: a run never charges more than it. `minimum` is `0` when cells can short-circuit (blank inputs, `run_when` rules, already-filled cells) or when unfound dataset rows are refunded. `charged_upfront` is `true` for dataset builds and extractions (taken at kickoff, refunded later where applicable) and `false` for enrichments and searches (accrued as work completes).

    `POST /monitors` charges nothing at creation; its estimate is the upper bound of **one** scheduled run over the monitor's `input`, and every scheduled run charges that again.
  # MCP server `instructions`. Deliberately short (~200 tokens max, enforced by
  # mcp-server/test/spec-contract.test.mjs): every MCP client injects this into
  # the model's context at session start, and some (Codex) repeat it once per
  # tool — with 26 tools, 2K tokens here became ~50K tokens of overhead. It
  # competes with the user's own context on every turn. Keep only cross-tool
  # behavior; anything about one tool belongs in that operation's description,
  # and setup / auth / SDK / migration text belongs in `description` above (the
  # docs site), which MCP clients never see.
  x-mcp-instructions: |
    Riveter builds lists (rows), enriches rows with new columns, scrapes pages, searches, and answers research questions.
    Async tools (`enrich`, `build_dataset`, ...) return a run `id` (`run_...`). Call `get_run_result` or `get_run` with `wait: 50` to long-poll (one request per 50 s). Prefer `webhook_url`.
    `scrape`, `quick_search`, `search_agent` are synchronous. Monitors re-run a saved enrichment on a schedule.
    A dataset (`ds_...`, beta per account) keeps rows and filled cells: `enrich` with `dataset_id` fills them; `list_dataset_rows` reads them. `help` → `datasets`.
    Paid tools accept `estimate_cost: true` (estimate only) and `max_credits` (refused if the estimate is higher).
    Call `help` before writing `output` columns or when an error names a limit.
    Ids: `run_`, `enr_`, `ds_`, `row_`, `ext_`, `mon_`. Auth comes from the MCP connection.
  # Reference docs served by the MCP `help` tool, one topic per key (the keys
  # become the tool's `topic` enum). Loaded only when the agent calls `help`,
  # so a topic may be long — but keep each one single-purpose. JSON examples
  # here are validated by spec/lib/external_api/mcp_help_examples_spec.rb
  # against the real /v1/enrich validator and by mcp-server/test against the
  # schema enums, so they cannot drift from the API.
  x-mcp-help:
    overview: |
      # Riveter, in one page

      Riveter turns lists into researched tables.

      - **List build** (`build_dataset`) — finds rows from a description ("top 50 US SaaS companies"). A list build returns identifier columns only (e.g. Company + Website) and researches nothing about the rows. On the datasets beta (per account; `help` → `datasets`) the same tool makes a dataset (`ds_...`) that keeps the rows and the cells an enrichment fills.
      - **Enrichment** (`enrich`) — takes rows (inline `input` or a `dataset_id`) and fills in new columns using an AI agent per cell, with web search, scraping, PDFs, LinkedIn tools, and more. This is where research happens.
      - **Scrape** (`scrape`) — one URL → clean text. Synchronous.
      - **Quick search** (`quick_search`) — one query → titles, URLs, snippets. Synchronous.
      - **Search agent** (`search_agent`) — one question → one AI-researched answer. Synchronous. Same agent that fills one enrichment cell.
      - **Extraction** (`create_extraction` then `run_extraction`) — recipe for pulling structured records out of a hard website (pagination, filters). Discovery is a paid, one-time step per site.
      - **Monitor** (`create_monitor`) — re-run a saved enrichment daily / weekly / monthly over fixed `input` rows, optionally with diff webhooks. Every scheduled run charges the enrichment price again.

      **Runs.** Every async kickoff returns a run (`run_...`). `get_run` = status, `get_run_result` = status + `output` (`wait: 50` long-polls), `stop_run` = cancel. Statuses: `pending` → `enqueued` → `processing` → `success` | `stopped`.

      **Typical flows**
      1. User has a list, wants columns → `enrich` (saved `enrichment_id` if one exists; else `prompt` + `attributes`; else a full `output` spec — see `help(enrich_modes)`).
      2. User has no list → `build_dataset` (rows) → `enrich` with the `dataset_id` (research). Or `build_dataset` with `auto_run_enrichment: true` to chain both. On the datasets beta: `build_dataset` with `count` + `attributes` and `auto_fill: true`.
      3. Repeatable research → build and tune the enrichment once, then always run it by `enrichment_id`.

      **Money.** Everything except `get_*` / `list_*` spends credits. Add `estimate_cost: true` to any paid call to see the estimate first; add `max_credits` to refuse anything above a ceiling. See `help(credits)`.
    enrich_modes: |
      # `enrich`: the three ways to say what columns to add

      Rows come from exactly one of `input` (inline) or `dataset_id` (`ds_...`: a completed list build, whose rows are the input and take a column config like `input`; or, on the datasets beta, a dataset, whose cells are filled by its own enrichment — no column config, see `help(datasets)`). With `input`, column config comes from exactly one of the three below.

      **`input` shape** — columnar. Keys are headers, values are equal-length string arrays, one position = one row:
      ```json
      { "input": { "Company": ["Apple", "Google"], "Website": ["apple.com", "google.com"] } }
      ```

      ## 1. `enrichment_id` — run a saved enrichment (preferred)
      Fixed, tested config → most consistent results. Up to 10,000 rows. `input` headers must match the enrichment's source columns (`get_enrichment` shows them under `input`). Find ids with `list_enrichments`.
      ```json
      { "enrichment_id": "enr_...", "input": { "Company Name": ["Acme Corp", "Tech Solutions Inc"] } }
      ```

      ## 2. `prompt` + `attributes` — auto-generated config
      Describe the job and name the output columns (max 20). Riveter drafts the full column config, then runs. No setup, but the config is regenerated every call, so results vary run to run. Up to 1,000 rows. Good for one-offs and for drafting an enrichment you will then save and tune.
      ```json
      {
        "prompt": "For each B2B SaaS company, find the CEO, employee count, and primary industry.",
        "attributes": ["CEO", "Employee Count", "Industry"],
        "input": { "Company": ["Apple", "Google"] }
      }
      ```
      Put qualifiers in the prompt (industry, geography, entity type) — each cell agent sees only its prompt and its row.

      ## 3. `output` — full column spec
      Exact control: per column you set the prompt, contexts, tools, format, and `run_when`. Up to 1,000 rows. See `help(column_config)` for every field and `help(examples)` for complete recipes.
      ```json
      {
        "input": { "Company": ["Apple", "Google"] },
        "output": {
          "Employee Count": { "prompt": "Find the current number of employees at this company.", "contexts": ["Company"], "format": "number" },
          "CEO": { "prompt": "Find the CEO's full name.", "contexts": ["Company"] }
        }
      }
      ```

      ## Save, then run by id
      To make mode 2 or 3 repeatable: create the enrichment in the Riveter UI (or from a finished list build with `create_enrichment` + `dataset_id`; on the datasets beta, save one with `create_enrichment` + `prompt` + `output`), tune it with `update_enrichment`, then run it with mode 1.

      ## After kickoff
      The response is a run. `get_run_result({ id, wait: 50 })` until `output` is non-null, or pass `webhook_url` and stop polling. `output` is columnar: `{ "Column": [{ "value": ... }, ...] }`, one entry per input row.
    column_config: |
      # Output column config (the values in `output: { "<Header>": { ... } }`)

      Two modes per column. Never mix their fields.

      ## Agent mode — an AI agent researches each cell
      | field | notes |
      |---|---|
      | `prompt` | Required. Self-contained instruction. The cell agent sees ONLY this prompt and the row's context values — name the entity type and any qualifiers ("this YC-backed fintech"). |
      | `contexts` | Required, non-empty. Headers whose values are given to the agent: input columns and/or EARLIER output columns. |
      | `tools` | Optional. Default `["web_search", "scrape", "pdf", "http_request"]`. Add `"image"` for image analysis. Use `["scrape"]` to force "only read this URL". `[]` = no tools (pure reasoning over contexts). |
      | `format` | Optional. `text` (default), `number`, `url`, `email`, `tag`, `date`, `json`, `boolean`. See `help(formats)`. |
      | `format_details` | Per-format options (tag options, JSON schema, ...). See `help(formats)`. |
      | `run_when` | Per-row gate. See `help(run_when)`. |
      | `max_tool_calls` | Optional cap on tool calls per cell (default 10). |

      ## Tool-only mode — one tool, no agent loop; cheaper and deterministic
      Set `tool` plus that tool's params. **Do not send `prompt`/`contexts` for tools that don't take them** — dependencies are inferred from param values that match a column header.
      ```json
      { "Homepage Text": { "tool": "scrape", "url": "Website" } }
      ```
      When a param value equals a column header it is per-row (that row's value); otherwise it is a static string. Tools and params: `help(tools)`.
      `run_when` / `run_when_config` work here too — gate an expensive tool call on an earlier column's value. Rule columns become dependencies automatically; do not add `contexts`. See `help(run_when)`.

      ## Ordering and dependencies
      Columns run in dependency order, and the order you write them matters: a column may only reference columns defined BEFORE it (via `contexts`, via tool params / `args` that name a column, or via a tool-only column's `run_when_config` rules). Pattern: extraction → analysis → synthesis → scoring.

      ## Rules that save credits and avoid bad data
      - Fetch a source once. One `scrape` / LinkedIn / PDF column, then agent columns with that column in `contexts` to extract pieces. Never three columns scraping the same page.
      - Group related fields from one source into one `json` column instead of N agent columns.
      - Prefer `text` / `number` / `boolean` over `json` when one value is enough.
      - For URL columns add "Only return a real URL found via search or scrape — do not guess."
      - Do not tell the agent to return null / N/A when not found — Riveter emits a "not found" value itself.
      - Chained columns: set `run_when: "any_filled"` so rows where the upstream column found nothing are skipped for free.
      - Input headers and output headers must all be unique.
    run_when: |
      # `run_when` — skip rows, chain columns

      A per-row gate checked before the cell runs, for agent columns AND tool-only columns. A skipped cell stays empty and costs **0 credits**.

      | value | runs when |
      |---|---|
      | `always` (default) | every row |
      | `any_filled` | at least one of the column's dependencies has a value ("not found" counts as empty). Dependencies = `contexts` for agent columns; column-mapped tool params for tool-only columns. The right default for any column that depends on an earlier output column. |
      | `all_filled` | every dependency has a value. Rarely wanted — one "not found" silently skips the row. |
      | `dynamic` | the `run_when_config` rules match. Use to branch on an earlier output's VALUE. |

      ## `run_when_config`
      ```json
      { "match_mode": "all", "rules": [{ "column": "CEO", "condition": "is_not_empty" }] }
      ```
      - `match_mode`: `all` (default) or `any`.
      - `rules[].column`: header of an input column or an EARLIER output column.
        - Agent column: **it must also be in this column's `contexts`** — the row's cell value is only loaded for context columns; a rule on a non-context column always sees empty.
        - Tool-only column: no `contexts` needed — every rule column is added to the column's dependencies automatically (it must still be defined BEFORE this column).
      - `rules[].condition`: `is_empty`, `is_not_empty` (no `value`); `text_contains`, `text_does_not_contain`, `text_starts_with`, `text_ends_with` (case-insensitive, need `value`); `text_is_exactly` (exact, case-sensitive, needs `value`).

      ## Example — skip an expensive tool-only lookup unless a cheap check passes
      ```json
      {
        "input": { "LinkedIn URL": ["https://linkedin.com/in/example"] },
        "output": {
          "Profile": { "tool": "linkedin_person_profile", "url": "LinkedIn URL" },
          "Seniority": { "prompt": "Classify this person's seniority from their profile.", "contexts": ["Profile"], "format": "tag", "format_details": { "options": ["ic", "manager", "executive"] }, "run_when": "any_filled" },
          "Work Email": {
            "tool": "linkedin_person_contact",
            "url": "LinkedIn URL",
            "run_when": "dynamic",
            "run_when_config": { "rules": [{ "column": "Seniority", "condition": "text_is_exactly", "value": "executive" }] }
          }
        }
      }
      ```
      `Work Email` only spends credits on rows where `Seniority` is `executive`.

      ## Example — look up an email only when a CEO was found; write notes only for enterprise companies
      ```json
      {
        "input": { "Company": ["Stripe", "Acme Corp"] },
        "output": {
          "CEO": { "prompt": "Find the CEO's full name.", "contexts": ["Company"] },
          "Company Size": { "prompt": "Classify this company's size.", "contexts": ["Company"], "format": "tag", "format_details": { "options": ["startup", "mid-market", "enterprise"] } },
          "CEO Email": {
            "prompt": "Find a work email address for this CEO.",
            "contexts": ["Company", "CEO"],
            "format": "email",
            "run_when": "dynamic",
            "run_when_config": { "rules": [{ "column": "CEO", "condition": "is_not_empty" }] }
          },
          "Enterprise Notes": {
            "prompt": "Summarize this company's enterprise offering, pricing model, and notable enterprise customers.",
            "contexts": ["Company", "Company Size"],
            "run_when": "dynamic",
            "run_when_config": { "rules": [{ "column": "Company Size", "condition": "text_is_exactly", "value": "enterprise" }] }
          }
        }
      }
      ```
      With `prompt` + `attributes`, Riveter sets `run_when` for you. To change gating on a saved enrichment, send the column's `run_when` / `run_when_config` through `update_enrichment`.
    formats: |
      # `format` and `format_details`

      Only set a format when the value has a natural type. `text` is the default and is fine for prose.

      | format | value | `format_details` keys |
      |---|---|---|
      | `text` | free text | — |
      | `number` | numeric | `decimal_places` (0–9), `currency_code` ("USD"), `commas` (bool), `percentage` (bool). `currency_code` and `percentage` are mutually exclusive. |
      | `url` | one URL | — |
      | `email` | one email | — |
      | `boolean` | true/false | — |
      | `tag` | one of a fixed set | `options` (required, string array), `allow_multiple` (bool), `descriptions` (map option → meaning) |
      | `date` | a date | `iso_8601: true`, or tokens `month` (M, MM, MMM, MMMM), `day` (D, DD, Do), `year` (YYYY, YY), `delimiter` |
      | `json` | structured object | `schema` (JSON Schema object) and/or `description` (natural-language shape) |

      ## `tag` example
      ```json
      { "Industry": { "prompt": "Classify this company's primary industry.", "contexts": ["Company", "Website"], "format": "tag", "format_details": { "options": ["SaaS", "Fintech", "Healthcare", "Other"], "descriptions": { "Other": "Anything not covered by the other tags" } } } }
      ```

      ## `json` schema rules
      - Root `type` must be `object` (or `array`). Riveter adds `additionalProperties: false` and the full `required` array for you (strict mode) — you may include them, but every property will be required either way.
      - Optional fields: make them nullable, `"type": ["string", "null"]`.
      - Add `minLength` to text fields you care about (reasoning ~50, descriptions ~20, names ~2) — `required` alone allows `""`.
      - Keep it flat: ≤ 5–6 properties per column. Split otherwise.
      - Not supported: `$ref`, `oneOf`, `allOf`, `not`, nesting > 10 levels. `anyOf` is supported. Do not use `maxItems` on arrays (it truncates data).
      ```json
      {
        "Pricing": {
          "prompt": "Find this product's pricing. Return the plan names, monthly price in USD, and whether a free tier exists.",
          "contexts": ["Product", "Website"],
          "format": "json",
          "format_details": {
            "schema": {
              "type": "object",
              "properties": {
                "plans": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string", "minLength": 2 }, "monthly_usd": { "type": ["number", "null"] } }, "required": ["name", "monthly_usd"], "additionalProperties": false } },
                "has_free_tier": { "type": "boolean" },
                "source_url": { "type": ["string", "null"] }
              },
              "required": ["plans", "has_free_tier", "source_url"],
              "additionalProperties": false
            }
          }
        }
      }
      ```
    tools: |
      # Tools

      ## Agent-mode `tools` (the agent decides when to call them)
      `web_search`, `scrape`, `pdf`, `image`, `http_request`, `check_urls`, `text_search_pdf`. Default when omitted: `["web_search", "scrape", "pdf", "http_request"]`. Each cell has a budget of tool calls (`max_tool_calls`, default 10). Tools cost credits inside the cell's price; keep the default set unless you have a reason.

      ## Tool-only `tool` (one deterministic call per row, no agent)
      Param values that equal a column header are per-row; anything else is static.

      | `tool` | required params | optional | returns |
      |---|---|---|---|
      | `scrape` | `url` | `proxy_country_code`, `wait_longer`, `skip_cache` | page text |
      | `web_search` | `query` | `date_start`, `date_end` (YYYY-MM-DD) | results with titles/urls/snippets |
      | `pdf` | `url`, `prompt` | — | answer extracted from the PDF |
      | `image` | `url`, `prompt` | — | answer about the image |
      | `http_request` | `url` | `method` (GET/POST), body fields | raw HTTP response (public APIs only) |
      | `code` | `code` | `args` | return value of your JS. Free (0 credits). Sandbox, no network. |
      | `linkedin_person_profile` | `url` (linkedin.com/in/<slug>) | — | profile data |
      | `linkedin_person_contact` | `url` | — | emails (waterfall) |
      | `linkedin_person_contact_lite` / `linkedin_person_phones_lite` | `url` | — | cheaper email / phone lookups |
      | `linkedin_person_posts` / `_comments` / `_reactions` / `_activities` | `url` | — | activity |
      | `linkedin_company_profile` / `_data` / `_job_count` / `_depth_chart` / `_posts` | `url` (linkedin.com/company/<slug>) | — | company data |
      | `linkedin_post_comments` / `linkedin_post_reactions` | `post_url` | `reaction_type` | post engagement |

      ## `code` tool
      `code` is a JavaScript function body; read inputs via `args.<name>`. `args` maps names to column headers (per-row) or static strings. Dependencies come from `args`, so a code column must be defined after the columns it reads.
      ```json
      {
        "Domain": { "tool": "code", "code": "try { return new URL(args.url).hostname.replace(/^www\\./, '') } catch (e) { return null }", "args": { "url": "Website" } },
        "Revenue per Employee": { "tool": "code", "code": "const r = parseFloat(args.revenue) || 0; const e = parseInt(args.employees) || 1; return (r / e).toFixed(2)", "args": { "revenue": "Annual Revenue", "employees": "Employee Count" } }
      }
      ```

      ## LinkedIn: always resolve the URL first
      Input columns called "LinkedIn URL", "Person URL", "Website" are usually NOT canonical LinkedIn URLs. Never pipe them straight into a `linkedin_*` tool. Add an agent column that returns the exact `https://linkedin.com/in/<slug>` (or `/company/<slug>`) URL, then point the tool-only column at that column. One resolver per entity type. Full recipe: `help(examples)` → "Person enrichment via LinkedIn".

      ## When to use which mode
      - Known URL + one tool (read a PDF, scrape a page, LinkedIn lookup) → tool-only.
      - Deterministic transform of existing columns → tool-only `code`.
      - Needs searching, reasoning, or more than one tool → agent mode.
    examples: |
      # Complete `enrich` payloads (copy, edit headers, run)

      Each block is a full tool call. Prefer these shapes over inventing new ones.

      ## Company basics
      ```json
      {
        "input": { "Company": ["Stripe", "Notion"], "Website": ["stripe.com", "notion.so"] },
        "output": {
          "Legal Name": { "prompt": "Find this company's registered legal name.", "contexts": ["Company", "Website"], "run_when": "any_filled" },
          "HQ City": { "prompt": "Find the city of this company's headquarters.", "contexts": ["Company", "Website"], "run_when": "any_filled" },
          "Employee Count": { "prompt": "Find the current number of employees.", "contexts": ["Company", "Website"], "format": "number", "run_when": "any_filled" },
          "Annual Revenue": { "prompt": "Find the most recent annual revenue in USD.", "contexts": ["Company", "Website"], "format": "number", "format_details": { "currency_code": "USD" }, "run_when": "any_filled" }
        }
      }
      ```

      ## Lead scoring (extract → analyze → score)
      ```json
      {
        "input": { "Company": ["Acme Agency"], "Website": ["acme.agency"] },
        "output": {
          "Services Offered": { "prompt": "List the services this agency offers, from its website.", "contexts": ["Company", "Website"], "tools": ["scrape", "web_search"] },
          "Is Webflow Agency": { "prompt": "Based on the services listed, is this primarily a Webflow design/development agency?", "contexts": ["Company", "Services Offered"], "tools": [], "format": "boolean", "run_when": "any_filled" },
          "ICP Score": {
            "prompt": "Score fit for our ICP (Webflow agencies, 5–50 staff, US/EU). Return score 0–100, a segment, and reasoning.",
            "contexts": ["Company", "Services Offered", "Is Webflow Agency"],
            "tools": [],
            "format": "json",
            "format_details": { "schema": { "type": "object", "properties": { "score": { "type": "number" }, "segment": { "type": "string", "enum": ["ideal", "possible", "poor"] }, "reasoning": { "type": "string", "minLength": 50 } }, "required": ["score", "segment", "reasoning"], "additionalProperties": false } },
            "run_when": "any_filled"
          }
        }
      }
      ```

      ## Person enrichment via LinkedIn (resolver → tool-only → synthesis)
      ```json
      {
        "input": { "Person Name": ["Tobias Lütke", "Patrick Collison"], "Qualifying Information": ["CEO of Shopify", "CEO of Stripe"] },
        "output": {
          "LinkedIn URL": {
            "prompt": "Find this person's canonical LinkedIn profile URL in the form https://linkedin.com/in/<slug>. Only return a real URL found via search or scrape — do not guess or fabricate one.",
            "contexts": ["Person Name", "Qualifying Information"],
            "tools": ["web_search", "scrape"],
            "format": "url",
            "run_when": "any_filled"
          },
          "LinkedIn Data": { "tool": "linkedin_person_profile", "url": "LinkedIn URL" },
          "Contact Data": { "tool": "linkedin_person_contact", "url": "LinkedIn URL" },
          "Bio": { "prompt": "Write a two-sentence professional bio of this person from the details provided.", "contexts": ["Person Name", "Qualifying Information", "LinkedIn Data"], "tools": [], "run_when": "any_filled" }
        }
      }
      ```
      For companies: same shape with a `/company/<slug>` resolver and `linkedin_company_profile`.

      ## PDF extraction (find the document → read it)
      ```json
      {
        "input": { "Company": ["Apple"] },
        "output": {
          "Annual Report URL": { "prompt": "Find the URL of this company's most recent annual report PDF (10-K or equivalent). Only return a real URL found via search.", "contexts": ["Company"], "format": "url" },
          "Reported Revenue": { "tool": "pdf", "url": "Annual Report URL", "prompt": "What was total net revenue for the most recent fiscal year? Return the number in USD." }
        }
      }
      ```
      If the PDF URL is already an input column, skip the first column.

      ## Product matching across retailers (one JSON column per retailer)
      ```json
      {
        "input": { "Product": ["Sony WH-1000XM5"], "Brand": ["Sony"] },
        "output": {
          "Amazon": {
            "prompt": "Check if this exact product is sold on amazon.com. Return match type, URL, list price, sale price, stock status, and review rating.",
            "contexts": ["Product", "Brand"],
            "format": "json",
            "format_details": { "schema": { "type": "object", "properties": { "match_type": { "type": "string", "enum": ["exact", "similar", "not_found"] }, "url": { "type": ["string", "null"] }, "list_price": { "type": ["number", "null"] }, "sale_price": { "type": ["number", "null"] }, "in_stock": { "type": "boolean" }, "review_rating": { "type": ["number", "null"] } }, "required": ["match_type", "url", "list_price", "sale_price", "in_stock", "review_rating"], "additionalProperties": false } }
          }
        }
      }
      ```

      ## Image analysis (URL known → tool-only)
      ```json
      {
        "input": { "Company": ["Stripe"], "Logo URL": ["https://stripe.com/img/v3/home/social.png"] },
        "output": { "Logo Colors": { "tool": "image", "url": "Logo URL", "prompt": "List the dominant colors in this logo as hex codes." } }
      }
      ```

      ## Transform with code (free)
      ```json
      {
        "input": { "First Name": ["Ada"], "Last Name": ["Lovelace"], "Website": ["https://www.example.com/about"] },
        "output": {
          "Full Name": { "tool": "code", "code": "return `${args.first} ${args.last}`", "args": { "first": "First Name", "last": "Last Name" } },
          "Domain": { "tool": "code", "code": "try { return new URL(args.url).hostname.replace(/^www\\./, '') } catch (e) { return null }", "args": { "url": "Website" } }
        }
      }
      ```
    credits: |
      # Credits

      Every paid tool accepts two optional fields:
      - `estimate_cost: true` — validate and return the estimate; nothing is created or charged. Response: `{ "estimate_cost": true, "type": "<run type>", "credit_detail": { "estimate": { "minimum", "maximum", "charged_upfront" }, "credits_used": 0, "credits_remaining", "sufficient_credits" } }`.
      - `max_credits` — ceiling. If the estimate's `maximum` exceeds it, the call is refused with `credit_cap_exceeded` and nothing is charged.

      Every run carries `credit_detail` on kickoff and on `get_run`. `maximum` is the contract — a run never charges more. `minimum` is 0 when cells can skip (blank inputs, `run_when`, already-filled cells). `charged_upfront` is true for dataset builds and extractions (taken at kickoff, unfound rows refunded), false for enrichments and searches (accrued as work completes).

      **Estimate or run?** You do not need a separate estimate call before a paid call: pass `max_credits` on the call itself and the API runs it when the estimate fits, else refuses it (`credit_cap_exceeded`) with the estimate in `credit_detail` — show that and ask. Use `estimate_cost: true` when the user asks what something would cost. When the user says to run without a cost check, run it without `max_credits`; `insufficient_credits` is the backstop.

      **Rough pricing** (use `estimate_cost` for exact numbers): an agent-mode cell = 1 credit; tool-only cells are priced per tool (scrape ≈ 0.10, web_search 0.04, pdf 2, image 0.4, LinkedIn 1.5–10 — the LinkedIn email / phone tools charge only when they return a contact, a miss is free); `code` = 0. A dataset build = 4 credits per row for the first 250 rows, then 2. `search_agent` = 1 per call; `quick_search` and `scrape` are fractional. Extraction discovery is a one-time charge per site; each extraction run is priced per plan — both shown by `estimate_cost`. `create_monitor` charges nothing itself; its `estimate_cost` prices ONE scheduled run over `input`, and every scheduled run charges that again (type `monitor`).

      **Cheapest correct enrichment:** saved `enrichment_id`, `run_when: any_filled` on chained columns, one fetch per source, `code` for transforms, `text`/`number` over `json` where one value suffices.
    datasets_vs_enrichment: |
      # List build vs enrichment

      This topic is the list build: `build_dataset` with `prompt` and / or `identifiers` + `qualifiers` and `max_items`, on every account. (On the datasets beta, `build_dataset` with `rows` or `count` makes a dataset that keeps its rows and cells; that is `help` → `datasets`.)

      **A list build finds rows. It does not research them.** `build_dataset` returns identifier columns only (e.g. Company + Website). `attributes` you pass are NOT filled in — they are saved as the output columns of an enrichment that runs later, as a separate paid step.

      To get attributes filled, either:
      - pass `auto_run_enrichment: true` on `build_dataset` (the response also carries `enrichment_run_id`; poll both runs), or
      - after the build succeeds, call `enrich` with the `dataset_id` and a config (`enrichment_id`, `prompt` + `attributes`, or `output`).

      A build result never contains attribute values. If a user asked for columns and you did not auto-enrich, the next step is `enrich`, not another `build_dataset`.

      In the Riveter app and in the API docs this is a **list build** ("Build a list" in the sidebar, past runs under "List builds"; the docs' "List builds" section); its finished rows open as an enrichment. The tool names keep the dataset vocabulary (`build_dataset`, `extend_dataset`, `build_dataset_for_enrichment`).

      Limits: `identifiers` ≤ 3, `qualifiers` ≤ 10, `attributes` ≤ 20. `max_items` is capped per account; `estimate_cost: true` shows the row estimate and price.

      `build_dataset_for_enrichment` builds rows shaped for a saved enrichment (identifiers derived from its input columns). `extend_dataset` adds new, deduplicated rows to a finished build.
    datasets: |
      # Datasets

      A dataset (`ds_...`) is a grid Riveter keeps for you: input columns (row identity: values you send or Riveter generates) and output columns (cells an enrichment fills). Rows are `row_...`. Columns are addressed by name everywhere; there are no column ids.

      **Beta, per account.** The dataset tools (the API docs' "Datasets (beta)" section) are turned on account by account. On an account without them every tool below answers `403 feature_not_enabled` — tell the user to email support@riveterhq.com and stop retrying. `build_dataset` with `max_items` (the list build) keeps working everywhere; see `help` → `datasets_vs_enrichment`.

      | tool | does |
      |---|---|
      | `build_dataset` | a new dataset with rows you send (`rows`, free) or rows Riveter generates (`prompt` + `count`, paid) |
      | `list_datasets` / `get_dataset` | the account's datasets; one dataset with its columns, `enrichment_id`, `row_count`, newest `runs` |
      | `add_dataset_rows` / `import_dataset_rows` | append rows as JSON objects or as CSV text (free) |
      | `build_dataset_rows` | more generated rows for an existing dataset (paid; needs a `index_column`) |
      | `list_dataset_rows` | rows in position order with every cell's `value` and `state` and the row's `credits`; page with `cursor` |
      | `enrich` with `dataset_id` | fill cells (paid). Default scope: every `pending`, `stale`, or `failed` cell. Narrow with `columns` (output column names), `rows` (row ids) and / or `positions` (row positions, from 1), or several such boxes at once as `targets`; pick the cell states to re-run with `states` (`not_found`, `ok`, ...); `force: true` is every state. A row is in one fill at a time: fills on disjoint rows run side by side, and one that overlaps a running fill is refused naming the run and the rows |
      | `update_dataset` | rename, set `index_column`, add / rename / remove input columns, add / change / rename / remove output columns, set the per-row `budget` or `budget_locked` |
      | `update_dataset_row` | write one row by hand, by id or position: `input` values (the dedupe key follows) and / or `cells` (`manual` cells); `null` clears one |
      | `promote_dataset_column` | an output column becomes an input column; every row's value moves with it |
      | `delete_dataset_rows` / `delete_dataset` | remove rows by `ids` and / or `positions` (up to 100 per call) / the dataset |
      | `export_dataset` | the grid as CSV |

      ## Output columns and the enrichment

      A dataset's output columns are the columns of its enrichment (`output_columns` lists their names on `get_dataset`; `get_enrichment` with the dataset's `enrichment_id` shows each column's `prompt` and `output_schema`, the task `prompt`, `tools`, `budget_credits_per_row`). `build_dataset` takes `attributes` (column names) or the `enrichment_id` of a saved enrichment (`create_enrichment` with `prompt` + `output`). An output column is `{ "name", "prompt"?, "output_schema"? }`; `output_schema` is a JSON Schema for the cell value, inferred from the prompts when you give none and the value is one plain value — an email, URL, date, number, or boolean (`help` with `output_schema`).

      `update_dataset` edits the dataset's own enrichment in place. A saved enrichment shared with other datasets is copied first so the other datasets do not change (the dataset's `enrichment_id` changes). The per-row budget starts at 1.5 credits per output column and moves by 1.5 as columns are added or removed; `budget_locked: true` (on `update_dataset` or `update_enrichment`) holds it, and sent in the same request as column changes it keeps the budget where it is.

      ## Cell states

      `pending` (never filled), `running` (a fill is on it), `ok`, `not_found` (researched, nothing found; the row's `not_found_reason` says what the agent looked for), `failed` (never finished: `reason` is `credit_limit`, `provider`, or `internal`, and `error` is a sentence to show; the next fill re-runs it). Stopping a run is not a failure: the values its row agents had recorded land as `ok`, the other cells stay as they were (`pending` ones stay pending, so the next fill runs them), `manual` (set by hand; a fill skips it unless named in `states` or `force`), `stale` (the column's prompt or schema changed after the cell was filled; the next fill re-runs it).

      ## Fill scope

      Every pending, stale, or failed cell:
      ```json
      { "dataset_id": "ds_018f6a70-1234-7abc-89ab-0123456789ab" }
      ```
      One column on one row, even if filled:
      ```json
      { "dataset_id": "ds_018f6a70-1234-7abc-89ab-0123456789ab", "columns": ["CEO"], "rows": ["row_018f6a70-aaaa-7abc-89ab-0123456789ab"], "force": true }
      ```
      Rows 10–12 by position (positions count from 1, as `list_dataset_rows` reports them; no round-trip for the ids):
      ```json
      { "dataset_id": "ds_018f6a70-1234-7abc-89ab-0123456789ab", "positions": [10, 11, 12] }
      ```
      Every `not_found` cell again, on rows 1–20 only (`states` picks the cell states to run; the default is `pending`, `stale`, `failed`; `force` is every state, so do not send both):
      ```json
      { "dataset_id": "ds_018f6a70-1234-7abc-89ab-0123456789ab", "positions": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], "states": ["not_found"] }
      ```
      Several boxes in one run — `targets` is a list of `{ columns, rows, positions }` objects (not combined with the flat fields); a box without rows is every row, without columns every column:
      ```json
      { "dataset_id": "ds_018f6a70-1234-7abc-89ab-0123456789ab", "targets": [{ "columns": ["CEO"], "positions": [3, 4] }, { "columns": ["HQ"], "positions": [20, 21, 22] }] }
      ```
      A row is in one fill at a time. Fills on disjoint rows run side by side; a fill whose rows overlap a running fill's is refused (422 `validation`: "active run run_... already covers rows at positions 3, 4; wait for it, or leave them out"), and a fill on every row is refused while any fill runs on the dataset.

      `estimate_cost: true` prices the scope: `maximum` is rows × the enrichment's `budget_credits_per_row`; `expected` (present once the enrichment has finished fills behind it) is rows × its average charge per row. While a fill runs, column edits answer 409 `dataset_in_use` / `enrichment_in_use`: wait for it or `stop_run`.

      The fill's run reports `total_rows`, `completed_rows`, `running_rows` in `progress`. When it finishes, `get_run_result` `output` is `{ "rows": [...] }`: only the rows the fill ran, each in the `list_dataset_rows` shape (`id`, `position`, `input`, `cells` limited to the filled columns, `credits`). For the live grid, call `list_dataset_rows`.

      ## Generated rows

      `build_dataset_rows` needs a `index_column` (set one with `update_dataset`); the generator already knows the existing rows and skips them. It may land fewer rows than `count`: the run's `progress` reports `requested_rows`, `generated_rows`, `added_rows`, `skipped_duplicate_rows`.

      ## Index column

      `index_column` (an input column; the first one by default) makes rows unique on that value, ignoring case and spacing: duplicate rows sent or generated are skipped (`skipped_duplicates` in the response).

      ## Examples

      Create with rows and two output columns, then `enrich` with the new `id`:
      ```json
      { "name": "Targets", "input_columns": ["Company", "Website"], "rows": [{ "Company": "Apple", "Website": "apple.com" }, { "Company": "Shopify" }], "attributes": ["CEO", "HQ city"] }
      ```
      Generate 50 rows and fill them when they land:
      ```json
      { "name": "SF coffee", "prompt": "Specialty coffee roasters in San Francisco", "count": 50, "attributes": ["Founded year", "Flagship cafe address"], "auto_fill": true }
      ```
      `update_dataset`: change a column's prompt (its cells go `stale`), add one, remove one:
      ```json
      { "output": { "CEO": { "prompt": "Full name of the current CEO" }, "Employee count": { "prompt": "Headcount as an integer", "output_schema": { "type": "integer" } }, "HQ city": { "delete": true } } }
      ```
      `update_dataset`: add an input column and make it the index column:
      ```json
      { "input_columns": { "Ticker": {} }, "index_column": "Ticker" }
      ```
      `update_dataset_row`: fix an input value, set one cell, clear another:
      ```json
      { "input": { "Website": "apple.com" }, "cells": { "CEO": "Tim Cook", "HQ city": null } }
      ```
    output_schema: |
      # `output_schema`

      On a dataset-engine enrichment every output column may carry `output_schema`, a JSON Schema the cell value must match. The schema lives on the column; cells filled before a schema change read `stale` and are re-run by the next fill.

      **Types are inferred when you give none.** A column added without `output_schema` — on `create_enrichment`, `build_dataset` `attributes`, `enrich` with an inline `output`, or a new name in `update_dataset` / `update_enrichment` `output` — is typed from the task prompt, the column name, and the column prompt, when its value is one plain value: `{ "type": "string", "format": "email" | "uri" | "date" | "date-time" }`, `{ "type": "integer" }`, `{ "type": "number" }`, `{ "type": "boolean" }`, or for a money amount `{ "type": "number", "description": "Amount in USD, ... convert when the source reports another currency" }` (the currency the prompts name, else USD). Anything else — free text, a list, a structure, a fixed set of tags — stays untyped. The inferred schema is on the column in the response (`get_enrichment`); change or clear it with `update_dataset` / `update_enrichment`. To keep a column free text no matter what, send `"output_schema": { "type": "string" }`. Changing an existing column's prompt never re-types it.

      What is accepted: `type` (`string`, `number`, `integer`, `boolean`, `array`, `object`), `enum`, `properties`, `required`, `items`, `description`, `minimum` / `maximum`, `maxItems`, `format`. Not accepted: `$ref`, `$defs`, `oneOf`, `allOf`, `not`, `if` / `then` / `else`. Objects are closed (Riveter adds `additionalProperties: false` and lists every property as required). Up to 10 levels of nesting and 20,000 characters when serialized. The column itself is nullable on the wire (`not_found` cells have `value: null`); do not add `"null"` to `type`.

      The research agent reads the schema in plain words next to the column name ("integer between 1800 and 2100", "one of: SMB, Enterprise", "date, YYYY-MM-DD" for `format: date`, "URL" for `format: uri`, "email address" for `format: email`), and a `description` on the column's schema is printed under it, line by line — use it for notes on the shape, such as when each `enum` value applies ("SMB: fewer than 100 employees").

      Each example is one entry of `output` on `create_enrichment`, or one operation of `output` on `update_dataset` / `update_enrichment`.

      A number:
      ```json
      { "name": "Employee count", "prompt": "Current headcount", "output_schema": { "type": "integer", "minimum": 0 } }
      ```
      A choice:
      ```json
      { "name": "Segment", "prompt": "Which segment fits the company best", "output_schema": { "type": "string", "enum": ["SMB", "Mid-market", "Enterprise"] } }
      ```
      An object:
      ```json
      { "name": "Funding", "prompt": "The latest funding round", "output_schema": { "type": "object", "properties": { "round": { "type": "string" }, "amount_usd": { "type": "number" }, "announced_on": { "type": "string", "format": "date" } }, "required": ["round", "amount_usd", "announced_on"] } }
      ```
      A list:
      ```json
      { "name": "Competitors", "prompt": "Up to five direct competitors", "output_schema": { "type": "array", "items": { "type": "string" }, "maxItems": 5 } }
      ```
      Free text (no schema; a summary is never typed):
      ```json
      { "name": "Summary", "prompt": "Two sentences on what the company sells and to whom" }
      ```
      Free text whatever the name suggests (an explicit schema is never replaced by an inferred one):
      ```json
      { "name": "Email", "prompt": "The best address to write to, or how to reach them otherwise", "output_schema": { "type": "string" } }
      ```
    limits: |
      # Limits

      | what | limit |
      |---|---|
      | `enrich` rows with `enrichment_id` | 10,000 per call |
      | `enrich` rows with `output` or `prompt` + `attributes` | 1,000 per call |
      | `attributes` (enrich and dataset endpoints) | 20 |
      | dataset `identifiers` | 3 |
      | dataset `qualifiers` | 10 |
      | dataset `max_items` | per-account cap; error message states it |
      | `get_run` / `get_run_result` `wait` | 0–50 seconds |
      | `list_*` `per_page` | 100 (`list_enrichments`: 50) |
      | agent tool calls per cell | `max_tool_calls`, default and max 10 |
      | JSON schema nesting | 10 levels |
      | rate limit | 30 requests / minute per endpoint group (`X-RateLimit-*` headers) |
      | dataset `rows` per `build_dataset` / `add_dataset_rows` call | 100 (more: `import_dataset_rows`) |
      | dataset input columns | 50, names up to 100 characters; a value up to 10,000 characters |
      | dataset output columns | 50 per enrichment; `prompt` up to 5,000 characters, `output_schema` up to 20,000 |
      | `delete_dataset_rows` `ids` + `positions` | 100 rows per call |
      | `list_dataset_rows` `limit` | 1,000 (default 100) |
      | `budget` (credits per row) | 0.1–100; default 1.5 per output column, moving by 1.5 as columns are added or removed until `budget_locked` |

      All list/array limits are also in each tool's schema as `maxItems` / `maximum`.
    webhooks: |
      # Webhooks

      Pass `webhook_url` on any run kickoff (dataset builds: `dataset_webhook_url`) and Riveter POSTs the finished run to it. Events: `run.completed`, `run.stopped`, `run.finished`. Failed deliveries are retried up to 2 times; the endpoint must return 2xx.

      Payload (legacy-shaped, shared with the first-generation API):
      ```json
      {
        "event": "run.completed",
        "run_key": "<uuid>",
        "status": "success",
        "enrichment_uuid": "<uuid>", "enrichment_name": "My Enrichment",
        "credits_used": 2.5,
        "completed_at": "2026-01-15T12:00:00Z",
        "formatted_data": { "Company": [{ "value": "Apple" }], "Revenue": [{ "value": "383000000000" }] },
        "status_details": { "...": "flat status fields" },
        "run_status": { "run_key": "...", "status": "success", "...": "..." }
      }
      ```
      `run_key` is the run id without the `run_` prefix; `formatted_data` is the same `output` that `get_run_result` returns (columnar `{ "Header": [{ "value" }] }`, or `{ "rows": [...] }` for a fill on a kept dataset). Extraction runs use `extraction_plan_run.*` events and add `extraction_plan_uuid`, `run_state`, `records_count`. Monitors deliver only when the monitor's alert rule passes (e.g. `on_diff`).

      Use webhooks instead of polling whenever the caller can receive HTTP. If you must poll, `get_run` or `get_run_result` with `wait: 50` is one request per 50 seconds; without `wait`, no more than one request every 10–20 seconds.
    errors: |
      # Errors

      Shape: `{ "error": { "type": "<type>", "message": "<what to fix>" } }`. The `message` names the field and the limit.

      | `type` | meaning | do |
      |---|---|---|
      | `bad_request` / `validation` | malformed or invalid config | read `message`; common causes: `contexts` on a tool-only column, a column referencing a later column, unknown header, too many attributes |
      | `not_found` | unknown id | check the prefix (`run_`, `enr_`, `ds_`, `ext_`, `mon_`) and that it belongs to this account |
      | `forbidden` | key lacks access | the connection's API key cannot use this resource |
      | `duplicate_run_key` | `run_key` already used | pick a new `run_key` or fetch the existing run |
      | `insufficient_credits` | balance too low | `credit_detail` shows `credits_remaining`; reduce rows/columns or ask the user to top up |
      | `credit_cap_exceeded` | estimate `maximum` > your `max_credits` | raise `max_credits`, or shrink the request; `credit_detail` has the estimate |
      | `not_implemented` | unsupported combination | use the alternative named in `message` |
      | `dataset_in_use` / `enrichment_in_use` / `row_in_use` (409) | a fill is running on the dataset, enrichment, or row | wait for it (`get_run`) or `stop_run`, then retry the edit |
      | `payload_too_large` (413) | body over 2 MB | split the rows across calls, or use `import_dataset_rows` |
      | `feature_not_enabled` (403) | the datasets beta is not on for this account | stop retrying; tell the user to email support@riveterhq.com. The list build (`build_dataset` with `max_items`) and every other tool still work |

      Authentication failures (`401`) use `{ "request_status": "error", "message": "...", "error_type": "unauthorized" }` — the MCP connection's key was revoked; the user must reconnect.

      A run that fails after kickoff reports `status: "stopped"` with an `error` on `get_run` — no error envelope, since the kickoff already returned 201.

      A `legacy` object in a response (`{ "notice", "successor", "docs" }`) means the call hit a first-generation endpoint (`/run_status`, `/build_dataset`, ...). It still worked; make the next call to the `successor` instead. MCP tools never hit legacy endpoints.
  version: 2.3.0
  contact:
    name: Riveter Support
    url: https://riveterhq.com
    email: support@riveterhq.com
servers:
  - url: https://api.riveterhq.com/v1
    description: Production server

security:
  - ApiKeyAuth: []

paths:
  /enrich:
    post:
      summary: enrich
      description: |
        Provide rows in `input`, tell Riveter what columns to add and a webhook to send data to, or get back a run to poll.

        There are three ways to tell Riveter what to add. This is in order of preference:

        1. Run an existing enrichment (enrichment_id) — preferred.
        2. Run with a prompt + attributes — describe the columns in natural language.
        3. Run with a full output spec (output) — define each column exactly.

        ## Input: inline columnar data

        `input` is a JSON object in columnar form:
        - Keys are column headers.
        - Values are arrays of strings. Every array must be the same length.
        - Each position across the arrays is one row.

        For example, `{ "Company": ["Apple", "Google"], "Website": ["apple.com", "google.com"] }` is two rows:

        | Company | Website |
        |---------|-----------|
        | Apple   | apple.com |
        | Google  | google.com |

        ## 1. run an existing enrichment (enrichment_id) (preferred)

        Pass the id of a saved enrichment (`enr_...`). First build and fine-tune it in the [Riveter UI](https://app.riveterhq.com/enrichments), then run it with new rows. This is the preferred option: the configuration is fixed and tested, so results are the most consistent. Max 10,000 rows per request. Input column headers must match the enrichment's source-data columns.

        ```bash
        curl -X POST "https://api.riveterhq.com/v1/enrich" \
          -H "Authorization: Bearer YOUR_API_KEY" \
          -H "Content-Type: application/json" \
          -d '{
            "enrichment_id": "enr_YOUR_ENRICHMENT_ID",
            "input": {"Company Name": ["Acme Corp", "Tech Solutions Inc"]}
          }'
        ```

        ## 2. run from a prompt + attributes

        Provide a natural-language `prompt` and an `attributes` array of output column names. The AI generates the full column configuration for you. This needs no setup, but the configuration is generated fresh each time, so results can vary between runs. For consistent, repeatable results, save the enrichment once and run it by `enrichment_id` (option 1). Max 1,000 rows per request, max 20 attributes.

        ```bash
        curl -X POST "https://api.riveterhq.com/v1/enrich" \
          -H "Authorization: Bearer YOUR_API_KEY" \
          -H "Content-Type: application/json" \
          -d '{
            "prompt": "Research each company",
            "attributes": ["CEO", "Employee Count", "Industry"],
            "input": {"Company": ["Apple", "Google"]},
            "webhook_url": "https://your-server.com/webhook"
          }'
        ```

        ## 3. run from a full output spec

        Define each output column exactly: the prompt, contexts, tools, format, and `run_when` (skip rows or chain columns — skipped cells cost 0 credits) per column. Use this when you need precise control over how each column runs. See the `EnrichmentOutputSpec` schema. Max 1,000 rows per request.

        ```bash
        curl -X POST "https://api.riveterhq.com/v1/enrich" \
          -H "Authorization: Bearer YOUR_API_KEY" \
          -H "Content-Type: application/json" \
          -d '{
            "output": {
              "Employee Count": {
                "prompt": "Find the number of employees at this company",
                "contexts": ["Company"],
                "format": "number"
              }
            },
            "input": {"Company": ["Apple", "Google"]}
          }'
        ```

        ## Rows from a dataset or a list build (`dataset_id`)

        Instead of inline `input`, pass `dataset_id` (`ds_...`). The row source is exactly one of `input` or `dataset_id`.

        - **A completed list build** ([POST /datasets](#tag/list-builds/post/datasets) with `max_items`): its rows are the input, and a column config is required as with `input` (one of the three options above).
        - **A dataset** (beta): fills the cells of the dataset. Its columns come from its bound enrichment, so no column config is sent (an `enrichment_id` is accepted only when it is that enrichment). By default every `pending`, `stale`, or `failed` cell runs; `columns` (output column names), `rows` (row ids), and `positions` narrow the scope, and `force: true` re-runs cells that are already filled. On an account without the datasets beta this answers `403 feature_not_enabled`.

        ```bash
        curl -X POST "https://api.riveterhq.com/v1/enrich" \
          -H "Authorization: Bearer YOUR_API_KEY" \
          -H "Content-Type: application/json" \
          -d '{
            "dataset_id": "ds_YOUR_DATASET_ID",
            "columns": ["CEO"]
          }'
        ```

        ## After the kickoff

        The response is a [run](#model/run). Poll [GET /runs/{id}](#tag/runs/get/runs/{id}), fetch results with [GET /runs/{id}/result](#tag/runs/get/runs/{id}/result), or just wait for your `webhook_url` to be called.
      operationId: enrich
      x-mcp-open-world: true
      # MCP tool description (replaces `description` above for the MCP server
      # only). The docs-site text carries curl samples and headings that cost
      # tokens on every tools/list; the deep material lives in `help` topics.
      x-mcp-description: |
        Add AI-researched columns to rows. Rows come from exactly one of `input` (columnar: `{ "Header": ["row1", "row2"] }`, all arrays equal length) or `dataset_id` (`ds_...`: a dataset, or a completed list build).

        With a dataset's `dataset_id` (beta, per account; else 403 `feature_not_enabled`): fills its cells with its bound enrichment; send no column config. Default scope: every `pending`, `stale`, or `failed` cell. `columns` (output column names), `rows` (row ids) and `positions` (row positions) narrow it; `force: true` re-runs filled cells. `help` with `datasets`.

        With `input`, or a list build's `dataset_id`, column config is exactly one of, in order of preference:
        1. `enrichment_id` — run a saved enrichment (`enr_...`, from `list_enrichments`). Fixed, tested config; most consistent. Up to 10,000 rows. Input headers must match the enrichment's input columns (`get_enrichment`).
        2. `prompt` + `attributes` — Riveter drafts the column config from a description and up to 20 output column names. No setup; results vary per run. Up to 1,000 rows.
        3. `output` — full per-column spec: `prompt`, `contexts`, `tools`, `format`, `run_when`, or a tool-only `tool`. Up to 1,000 rows. Call `help` with `column_config`, `run_when`, `formats`, `tools`, or `examples` before writing one.

        Returns a run. Poll `get_run_result` with `wait: 50` (or pass `webhook_url`); a dataset fill's result is `{ "rows": [...] }`, only the rows it ran. `estimate_cost: true` returns the credit estimate without running.
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const run = await riveter.enrich({
              enrichment_id: "enr_YOUR_ENRICHMENT_ID",
              input: { "Company Name": ["Acme Corp", "Tech Solutions Inc"] },
            });
            const result = await riveter.runs.waitForResult(run.id);
            console.log(result.output);
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            run = riveter.enrich(
                enrichment_id="enr_YOUR_ENRICHMENT_ID",
                input={"Company Name": ["Acme Corp", "Tech Solutions Inc"]},
            )
            result = riveter.runs.wait_for_result(run.id)
            print(result.output)
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            run = riveter.enrich(
              enrichment_id: "enr_YOUR_ENRICHMENT_ID",
              input: { "Company Name" => ["Acme Corp", "Tech Solutions Inc"] }
            )
            result = riveter.runs.wait_for_result(run.id)
            puts result.output
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            ctx := context.Background()
            run, err := client.Enrich(ctx, riveter.EnrichParams{
                EnrichmentID: "enr_YOUR_ENRICHMENT_ID",
                Input:        map[string][]string{"Company Name": {"Acme Corp", "Tech Solutions Inc"}},
            })
            result, err := client.Runs.WaitForResult(ctx, run.ID, nil)
            fmt.Println(string(result.Output))
      tags:
        - Enrich
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                enrichment_id:
                  type: string
                  description: "Config source: id of a saved enrichment (enr_...)"
                output:
                  $ref: "#/components/schemas/EnrichmentOutputSpec"
                  description: "Config source: full column spec"
                prompt:
                  type: string
                  description: "Config source: natural-language instructions (requires attributes)"
                attributes:
                  type: array
                  items:
                    type: string
                  maxItems: 20
                  description: "Config source: output column names to auto-generate (requires prompt). Max 20."
                input:
                  $ref: "#/components/schemas/EnrichmentInputData"
                  description: "Row source: inline columnar data"
                dataset_id:
                  type: string
                  description: "Row source: a ds_... id. A dataset (beta): fills its cells with its bound enrichment, no column config with it. A completed list build: its rows are the input and take a column config."
                columns:
                  type: array
                  items:
                    type: string
                  maxItems: 50
                  description: "With dataset_id: only these output columns (by name). Default: every output column."
                rows:
                  type: array
                  items:
                    type: string
                  maxItems: 100000
                  description: "With dataset_id: only these rows (row_... ids). May be combined with positions. Default: every row."
                positions:
                  type: array
                  items:
                    type: integer
                    minimum: 1
                  maxItems: 100000
                  description: "With dataset_id: only the rows at these positions (from 1, as list_dataset_rows reports them). May be combined with rows."
                targets:
                  type: array
                  maxItems: 100
                  items:
                    type: object
                    properties:
                      columns:
                        type: array
                        items:
                          type: string
                        maxItems: 50
                        description: "Output column names in this box. Default: every output column."
                      rows:
                        type: array
                        items:
                          type: string
                        maxItems: 100000
                        description: "row_... ids in this box. May be combined with positions. Default: every row."
                      positions:
                        type: array
                        items:
                          type: integer
                          minimum: 1
                        maxItems: 100000
                        description: "Row positions (from 1) in this box. May be combined with rows."
                  description: "With dataset_id: several boxes of columns × rows in one run, e.g. CEO on rows 3-4 and HQ on rows 20-22. The scope is their union. Not combined with the flat columns / rows / positions."
                states:
                  type: array
                  items:
                    type: string
                    enum: [pending, stale, failed, not_found, ok, manual]
                  description: 'With dataset_id: the cell states to run, e.g. ["not_found"] to research the not-found cells again. Default: pending, stale, failed. Not combined with force.'
                force:
                  type: boolean
                  default: false
                  description: "With dataset_id: re-run every cell in scope, whatever its state (the same as states naming every state)."
                run_key:
                  type: string
                  maxLength: 255
                  pattern: "^[A-Za-z0-9._~-]+$"
                  description: |
                    Optional idempotency key, unique per account. Becomes the run id ("run_<run_key>"),
                    so it is limited to letters, digits, and . _ ~ - characters. A duplicate returns 409.
                webhook_url:
                  type: string
                  format: uri
                  description: URL to POST the results to when the run completes
                allow_duplicate_input:
                  type: boolean
                  default: false
                  description: With enrichment_id — allow re-running rows already present in the enrichment
                estimate_cost:
                  $ref: "#/components/schemas/EstimateCostParam"
                max_credits:
                  $ref: "#/components/schemas/MaxCreditsParam"
            examples:
              saved_enrichment:
                summary: Run a saved enrichment
                value:
                  enrichment_id: "enr_018f5b60-1234-7abc-89ab-0123456789ab"
                  input:
                    "Company Name": ["Acme Corp", "Tech Solutions Inc"]
              prompt_and_attributes:
                summary: Prompt + attributes (auto-generated config)
                value:
                  prompt: "Research each company"
                  attributes: ["CEO", "Employee Count", "Industry"]
                  input:
                    "Company": ["Apple", "Google"]
                  webhook_url: "https://your-server.com/webhook"
              full_output_spec:
                summary: Full output specification
                value:
                  output:
                    "Employee Count":
                      prompt: "Find the number of employees at this company"
                      contexts: ["Company"]
                      format: "number"
                  input:
                    "Company": ["Apple", "Google"]
              fill_dataset:
                summary: Fill a dataset's pending, stale, and failed cells
                value:
                  dataset_id: "ds_018f6a70-1234-7abc-89ab-0123456789ab"
              fill_dataset_scope:
                summary: Re-run one column on two rows of a dataset
                value:
                  dataset_id: "ds_018f6a70-1234-7abc-89ab-0123456789ab"
                  columns: ["CEO"]
                  rows:
                    [
                      "row_018f6a70-aaaa-7abc-89ab-0123456789ab",
                      "row_018f6a70-bbbb-7abc-89ab-0123456789ab",
                    ]
                  force: true
              fill_dataset_positions:
                summary: Fill rows 10–12 of a dataset by position
                value:
                  dataset_id: "ds_018f6a70-1234-7abc-89ab-0123456789ab"
                  positions: [10, 11, 12]
              cost_estimate:
                summary: Price the run without starting it
                value:
                  enrichment_id: "enr_018f5b60-1234-7abc-89ab-0123456789ab"
                  input:
                    "Company Name": ["Acme Corp", "Tech Solutions Inc"]
                  estimate_cost: true
      responses:
        "201":
          description: Run started — poll /runs/{id} or wait for the webhook
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Run"
        "200":
          description: estimate_cost only — the credit estimate; nothing was started
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EstimateCostResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /runs/{id}:
    get:
      summary: run status
      description: |
        This returns the **status and progress** of a run for any run created from enrichments, datasets, or extractions. `status` reports the status of the run, and `progress` gives a completion estimate.

        `?wait=N` (max 50) long-polls: the request holds until the run finishes or the budget elapses.
      operationId: getRun
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const run = await riveter.runs.get("run_YOUR_RUN_ID");
            console.log(run.status, run.progress);
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            run = riveter.runs.get("run_YOUR_RUN_ID")
            print(run.status, run.progress)
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            run = riveter.runs.get("run_YOUR_RUN_ID")
            puts run.status
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            run, err := client.Runs.Get(context.Background(), "run_YOUR_RUN_ID")
            fmt.Println(run.Status)
      tags:
        - Runs
      parameters:
        - name: id
          in: path
          required: true
          description: The run id (run_...)
          schema:
            type: string
        - name: wait
          in: query
          required: false
          description: Seconds to long-poll (0–50, default 0)
          schema:
            type: integer
            minimum: 0
            maximum: 50
            default: 0
      responses:
        "200":
          description: The run
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Run"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /runs/{id}/result:
    get:
      summary: run result
      description: |
        The run plus `output`. `output` is `null` until the run reaches a terminal state..

        Pass `?wait=N` (max 50) to long-poll: the request holds until the run finishes or the budget elapses, whichever comes first.

        ## Output shape by run type
        - **enrichment** — an object mapping column headers to arrays of cell objects: `{ "Company": [{"value": "Apple"}], "CEO": [{"value": "Tim Cook"}] }`
        - **dataset_build** — an object mapping column headers to arrays of cell objects (same columnar shape as enrichment)
        - **extraction** — the extracted records as an array of JSON objects (matching your `output_record_json_schema`)
        - **quick_search** — the search result object `{ "results": [{ "title", "link", "snippet" }, ...], "knowledge_graph"? }`, the same data the synchronous `POST /quick_search` response already carried. (Runs started on the legacy async `/web_search` endpoint share this run type but return the columnar enrichment shape with a `search_results` column.)
        - **search_agent** — the answer object `{ "result": <string or object> }`; `result` matches the request's `output_schema` when one was given, otherwise it's free text.
        - **enrichment** on a dataset (`POST /enrich` with `dataset_id`) — `{ "rows": [...] }`: only the rows the fill ran, in position order, each in the `/datasets/{id}/rows` shape (`id`, `position`, `input`, `cells` limited to the columns the fill covered, `credits`). `null` until the run is terminal; `GET /datasets/{id}/rows` is the live view. Above 5,000 rows, or after the rows expire (30 days), `output` is `null` and `output_files` (signed CSV and JSONL links to the whole grid; `pending` until the export lands, valid one hour, see `expires_at`) are the output.
        - **enrichment** started on `POST /enrich_rows`, or on `POST /enrich` with `input` — the columnar shape while rows are ≤ 5,000, plus the same `output_files`. Above 5,000 rows, or after the rows expire (30 days), `output` is `null` and the files are the output.
      operationId: getRunResult
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            // One 30s long-poll:
            const result = await riveter.runs.result("run_YOUR_RUN_ID", { wait: 30 });
            // Or keep polling until the run finishes (default budget 10 min):
            const finished = await riveter.runs.waitForResult("run_YOUR_RUN_ID");
            console.log(finished.output);
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            # One 30s long-poll:
            result = riveter.runs.result("run_YOUR_RUN_ID", wait=30)
            # Or keep polling until the run finishes (default budget 10 min):
            finished = riveter.runs.wait_for_result("run_YOUR_RUN_ID")
            print(finished.output)
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            # One 30s long-poll:
            result = riveter.runs.result("run_YOUR_RUN_ID", wait: 30)
            # Or keep polling until the run finishes (default budget 10 min):
            finished = riveter.runs.wait_for_result("run_YOUR_RUN_ID")
            puts finished.output
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            ctx := context.Background()
            // One 30s long-poll:
            result, err := client.Runs.Result(ctx, "run_YOUR_RUN_ID", &riveter.ResultOptions{Wait: 30})
            // Or keep polling until the run finishes (default budget 10 min):
            finished, err := client.Runs.WaitForResult(ctx, "run_YOUR_RUN_ID", nil)
            fmt.Println(string(finished.Output))
      tags:
        - Runs
      parameters:
        - name: id
          in: path
          required: true
          description: The run id (run_...)
          schema:
            type: string
        - name: wait
          in: query
          required: false
          description: Long-poll budget in seconds (0–50, default 0)
          schema:
            type: integer
            minimum: 0
            maximum: 50
            default: 0
      responses:
        "200":
          description: The run with output (null while still running)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Run"
                  - type: object
                    properties:
                      output:
                        description: The run's results (null until ready) — shape depends on run type, see the endpoint description
                        type: [object, array, "null"]
                      output_files:
                        $ref: "#/components/schemas/OutputFiles"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /runs/{id}/stop:
    post:
      summary: stop run
      description: |
        Stop a run early. Works for every run type. Already-finished runs are left untouched; the response is the run either way.
      operationId: stopRun
      x-mcp-destructive: true
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const run = await riveter.runs.stop("run_YOUR_RUN_ID");
            console.log(run.status); // "stopped"
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            run = riveter.runs.stop("run_YOUR_RUN_ID")
            print(run.status)  # "stopped"
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            run = riveter.runs.stop("run_YOUR_RUN_ID")
            puts run.status # "stopped"
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            run, err := client.Runs.Stop(context.Background(), "run_YOUR_RUN_ID")
            fmt.Println(run.Status) // "stopped"
      tags:
        - Runs
      parameters:
        - name: id
          in: path
          required: true
          description: The run id (run_...)
          schema:
            type: string
      responses:
        "200":
          description: The run after the stop
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Run"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /runs:
    get:
      summary: list runs
      description: |
        List the account's runs, newest first. Every async operation shows up here — enrichment runs, dataset builds, extractions, and quick searches.

        Filter by `type` (comma-separated), `status`, `enrichment_id`, `monitor_id`, and `created_after` / `created_before` (ISO 8601). Paginate with `page` / `per_page`.

        Pass `ids` (comma-separated, up to 100 run ids) to get those runs' full status objects (same shape as `GET /runs/{id}`) in one call; `not_found` lists ids not on this account. Filters and pagination are ignored.
      operationId: listRuns
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const page = await riveter.runs.list({ status: "success" });
            for await (const run of page) { // auto-pages through every result
              console.log(run.id, run.type);
            }
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            page = riveter.runs.list(status="success")
            for run in page.auto_paging_iter():  # pages through every result
                print(run.id, run.type)
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            page = riveter.runs.list(status: "success")
            page.auto_paging_each do |run| # pages through every result
              puts "#{run.id} #{run.type}"
            end
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            ctx := context.Background()
            page, err := client.Runs.List(ctx, &riveter.ListRunsParams{Status: "success"})
            for {
                for _, run := range page.Runs {
                    fmt.Println(run.ID, run.Type)
                }
                if !page.HasNextPage() {
                    break
                }
                page, err = page.NextPage(ctx)
            }
      tags:
        - Runs
      parameters:
        - name: ids
          in: query
          required: false
          description: "Comma-separated run ids, up to 100. Returns their full status objects plus `not_found`; other filters ignored."
          schema:
            type: string
        - name: type
          in: query
          required: false
          description: "Comma-separated run types: enrichment, dataset_build, extraction, scrape, quick_search, search_agent"
          schema:
            type: string
        - name: status
          in: query
          required: false
          description: Filter by run status
          schema:
            type: string
            enum: [pending, enqueued, processing, success, stopped]
        - name: enrichment_id
          in: query
          required: false
          description: Only runs of this enrichment (enr_...)
          schema:
            type: string
        - name: monitor_id
          in: query
          required: false
          description: Only runs of this monitor (mon_...)
          schema:
            type: string
        - name: created_after
          in: query
          required: false
          schema:
            type: string
            format: date-time
        - name: created_before
          in: query
          required: false
          schema:
            type: string
            format: date-time
        - name: page
          in: query
          required: false
          schema:
            type: integer
            default: 1
        - name: per_page
          in: query
          required: false
          schema:
            type: integer
            default: 25
            maximum: 100
      responses:
        "200":
          description: Runs listed (with `ids`, the named runs' full status envelopes plus `not_found`)
          content:
            application/json:
              schema:
                type: object
                properties:
                  runs:
                    type: array
                    description: Compact list items, or full `Run` objects when `ids` was passed
                    items:
                      oneOf:
                        - $ref: "#/components/schemas/RunListItem"
                        - $ref: "#/components/schemas/Run"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
                  not_found:
                    type: array
                    description: With `ids` only — the requested ids that are not runs on this account
                    items:
                      type: string
                required:
                  - runs
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /runs/summary:
    get:
      summary: list runs (summary)
      description: |
        All-time run counts by status — a snapshot of the account's run queue. For the runs themselves use [GET /runs](#tag/runs/get/runs)`?status=...`.
      operationId: runsSummary
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const summary = await riveter.runs.summary();
            console.log(summary.counts);
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            summary = riveter.runs.summary()
            print(summary.counts)
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            summary = riveter.runs.summary
            puts summary.counts
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            summary, err := client.Runs.Summary(context.Background())
            fmt.Printf("%+v\n", summary.Counts)
      tags:
        - Runs
      responses:
        "200":
          description: Run counts by status
          content:
            application/json:
              schema:
                type: object
                properties:
                  counts:
                    type: object
                    properties:
                      pending:
                        type: integer
                      enqueued:
                        type: integer
                      processing:
                        type: integer
                      success:
                        type: integer
                      stopped:
                        type: integer
              example:
                counts:
                  pending: 0
                  enqueued: 1
                  processing: 2
                  success: 40
                  stopped: 3
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /enrichments/{id}:
    get:
      summary: read enrichment
      description: |
        The enrichment: the task `prompt`, the `input_columns` its rows must carry (empty = any headers), its `output` columns (`name`, `prompt`, `output_schema`), `tools`, `budget_credits_per_row`, and `version` (goes up on every column change; a dataset cell filled under an older version reads `stale`).

        A legacy enrichment (one built in the UI's enrichment grid, or from a dataset build) answers its older shape: `input` (an object of source-data columns) and `output` as the column spec [POST /enrich](#tag/enrich/post/enrich) takes.
      operationId: getEnrichment
      x-mcp-description: |
        One enrichment by id: `prompt`, `input_columns` (the headers rows must carry), `output` columns (`name`, `prompt`, `output_schema`), `tools`, `budget_credits_per_row`, `version`. A legacy enrichment answers `input` (object) and a full `output` column spec instead.
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const enrichment = await riveter.enrichments.get("enr_YOUR_ENRICHMENT_ID");
            console.log(enrichment.input, enrichment.output);
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            enrichment = riveter.enrichments.get("enr_YOUR_ENRICHMENT_ID")
            print(enrichment.input, enrichment.output)
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            enrichment = riveter.enrichments.get("enr_YOUR_ENRICHMENT_ID")
            puts enrichment.input.inspect
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            enrichment, err := client.Enrichments.Get(context.Background(), "enr_YOUR_ENRICHMENT_ID")
            fmt.Println(enrichment.Input)
      tags:
        - Enrich
      parameters:
        - name: id
          in: path
          required: true
          description: The enrichment id (enr_...)
          schema:
            type: string
      responses:
        "200":
          description: The enrichment
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Enrichment"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      summary: update enrichment
      description: |
        Edit an enrichment in place: `name`, `prompt`, `input_columns` (the headers its rows must carry; `[]` = any), `budget` and `budget_locked`, `only_tools` / `additional_tools`, and `output` columns by name. Inside `output`, a name not yet on the enrichment adds a column (typed from the prompts when it has no `output_schema` and its value is one plain value); `{ "prompt", "output_schema" }` change one; `{ "rename": "New name" }` renames one (cells keep their values); `{ "delete": true }` removes one. Every call is one write: `version` goes up by one and every added or changed column takes it, so cells filled under the old prompt or schema read `stale` on datasets bound to this enrichment and are re-run by their next fill. A rename changes nothing on the cells.

        The budget follows the columns: adding a column raises `budget_credits_per_row` by 1.5, removing one lowers it by 1.5 (within 0.1–100), unless `budget_locked` is true or the same call sets `budget`. Set `budget_locked: true` to keep the budget as set from then on.

        Refused with 409 `enrichment_in_use` while a fill runs on it (the fill reads the recipe as it goes): wait for the run or [stop it](#tag/runs/post/runs/{id}/stop). To edit the columns of one dataset without touching other datasets on the same enrichment, use [PATCH /datasets/{id}](#tag/datasets-beta/patch/datasets/{id}), which copies a shared enrichment first.

        A legacy enrichment (UI grid) keeps its older contract: `output` holds the column spec of [POST /enrich](#tag/enrich/post/enrich) and `column_order` reorders.
      operationId: updateEnrichment
      x-mcp-destructive: true
      x-mcp-description: |
        Edit an enrichment: `name`, `prompt`, `input_columns`, `budget` (credits per row; moves by 1.5 per output column added or removed unless `budget_locked`), `only_tools` / `additional_tools`, and `output` columns by name: a new name adds a column (type inferred unless `output_schema` is given), `{ "prompt", "output_schema" }` changes one, `{ "rename" }` renames, `{ "delete": true }` removes. Changed columns make dataset cells `stale` (re-run by the next `enrich`). 409 `enrichment_in_use` while a fill runs. Prefer `update_dataset` to change one dataset's columns. `help` with `output_schema`.
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const changes = await riveter.enrichments.update("enr_YOUR_ENRICHMENT_ID", {
              output: {
                CEO: { prompt: "Find the CEO's full name", contexts: ["Company"] },
              },
            });
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            changes = riveter.enrichments.update(
                "enr_YOUR_ENRICHMENT_ID",
                output={"CEO": {"prompt": "Find the CEO's full name", "contexts": ["Company"]}},
            )
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            changes = riveter.enrichments.update(
              "enr_YOUR_ENRICHMENT_ID",
              output: { "CEO" => { prompt: "Find the CEO's full name", contexts: ["Company"] } }
            )
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            changes, err := client.Enrichments.Update(context.Background(), "enr_YOUR_ENRICHMENT_ID",
                riveter.UpdateEnrichmentParams{
                    Output: map[string]riveter.OutputColumnConfig{
                        "CEO": {Prompt: "Find the CEO's full name", Contexts: []string{"Company"}},
                    },
                })
      tags:
        - Enrich
      parameters:
        - name: id
          in: path
          required: true
          description: The enrichment id (enr_...)
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 200
                  description: Display name. An empty string clears it.
                prompt:
                  type: string
                  maxLength: 50000
                  description: The task prompt the row agent reads for every row.
                input_columns:
                  type: array
                  items:
                    type: string
                  maxItems: 50
                  description: The headers rows must carry when this enrichment runs (replaces the list; [] = any headers). Names only; no row data.
                budget:
                  type: number
                  minimum: 0.1
                  maximum: 100
                  description: Credits the row agent may spend per row. Sent with column changes, it wins over the per-column adjustment.
                budget_locked:
                  type: boolean
                  description: true keeps the budget as set when output columns are added or removed; false lets it move by 1.5 per column again.
                only_tools:
                  type: array
                  items:
                    type: string
                  description: Exactly these tools (names from GET /tools). Replaces the default set.
                additional_tools:
                  type: array
                  items:
                    type: string
                  description: The default tools plus these (e.g. linkedin_person_profile once enabled on the account; custom_apollo_people_search once enabled and your Apollo key is linked). See GET /v1/tools.
                output:
                  $ref: "#/components/schemas/OutputColumnOperations"
            examples:
              change_and_add_columns:
                summary: Change one column's prompt, add a typed one
                value:
                  output:
                    "CEO":
                      prompt: "Full name of the current CEO"
                    "Employee count":
                      prompt: "Current headcount as an integer"
                      output_schema: { type: integer, minimum: 0 }
              rename_and_delete:
                summary: Rename a column, delete another
                value:
                  output:
                    "HQ":
                      rename: "HQ city"
                    "Old Column":
                      delete: true
              retune:
                summary: New prompt and budget
                value:
                  prompt: "Research each company's go-to-market motion"
                  budget: 8
      responses:
        "200":
          description: The enrichment after the edit (its `version` went up)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Enrichment"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
    delete:
      summary: delete enrichment
      description: |
        Delete an enrichment. Runs in flight finish, and a dataset bound to it keeps its columns. Enrichments built in the app cannot be deleted here.
      operationId: deleteEnrichment
      x-badges:
        - name: Beta
          color: "#e9d5ff"
      x-mcp-destructive: true
      x-mcp-description: |
        Datasets beta, per account (else 403 `feature_not_enabled`). Delete an enrichment by id. Runs in flight finish; a dataset bound to it keeps its columns. Legacy enrichments cannot be deleted here.
      tags:
        - Enrich
      parameters:
        - name: id
          in: path
          required: true
          description: The enrichment id (enr_...)
          schema:
            type: string
      responses:
        "204":
          description: Deleted
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /enrichments/{id}/datasets:
    post:
      summary: enrichment → list build
      description: |
        Build a list **shaped for this enrichment**: identifiers are derived from the enrichment's source-data columns automatically, so generated rows land as valid input rows. The build finds rows only; the enrichment's output columns are not researched until the enrichment runs.

        Optionally set `auto_run_enrichment: true` to run the enrichment on the rows as soon as the build completes (a second paid run; the kickoff response then carries `enrichment_run_id`).

        Returns the dataset-build run — poll it via [GET /runs/{id}](#tag/runs/get/runs/{id}).
      operationId: buildDatasetForEnrichment
      x-mcp-open-world: true
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const run = await riveter.enrichments.buildDataset("enr_YOUR_ENRICHMENT_ID", {
              prompt: "US fintech startups",
              max_items: 100,
              auto_run_enrichment: true,
            });
            const result = await riveter.runs.waitForResult(run.id);
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            run = riveter.enrichments.build_dataset(
                "enr_YOUR_ENRICHMENT_ID",
                prompt="US fintech startups",
                max_items=100,
                auto_run_enrichment=True,
            )
            result = riveter.runs.wait_for_result(run.id)
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            run = riveter.enrichments.build_dataset(
              "enr_YOUR_ENRICHMENT_ID",
              prompt: "US fintech startups",
              max_items: 100,
              auto_run_enrichment: true
            )
            result = riveter.runs.wait_for_result(run.id)
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            ctx := context.Background()
            run, err := client.Enrichments.BuildDataset(ctx, "enr_YOUR_ENRICHMENT_ID",
                riveter.BuildDatasetForEnrichmentParams{
                    Prompt:            "US fintech startups",
                    MaxItems:          100,
                    AutoRunEnrichment: true,
                })
            result, err := client.Runs.WaitForResult(ctx, run.ID, nil)
      tags:
        - List builds
      parameters:
        - name: id
          in: path
          required: true
          description: The enrichment id (enr_...)
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                prompt:
                  type: string
                  description: What rows to generate (e.g. "US-based fintech startups")
                qualifiers:
                  type: array
                  items:
                    type: string
                  maxItems: 10
                  description: Optional constraints each row must satisfy (e.g. "B2B", "founded after 2015"). Max 10.
                max_items:
                  type: integer
                  description: Max rows to generate (capped by your plan)
                dataset_webhook_url:
                  type: string
                  format: uri
                  description: URL to POST the dataset results to when the build completes
                auto_run_enrichment:
                  type: boolean
                  default: false
                  description: Run the enrichment automatically when the build completes
                auto_run_enrichment_webhook_url:
                  type: string
                  format: uri
                  description: Webhook for the auto-run enrichment results
                estimate_cost:
                  $ref: "#/components/schemas/EstimateCostParam"
                max_credits:
                  $ref: "#/components/schemas/MaxCreditsParam"
              required:
                - prompt
            examples:
              build_and_auto_run:
                summary: Build rows and auto-run the enrichment
                value:
                  prompt: "US-based fintech startups"
                  qualifiers: ["B2B", "founded after 2015"]
                  max_items: 100
                  auto_run_enrichment: true
      responses:
        "201":
          description: Dataset build started — the run plus dataset_id / enrichment_id / max_items (and enrichment_run_id when auto-running)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Run"
                  - type: object
                    properties:
                      dataset_id:
                        type: string
                      enrichment_id:
                        type: string
                      max_items:
                        type: integer
                      enrichment_run_id:
                        type: string
                        description: Present when auto_run_enrichment is true
        "200":
          description: estimate_cost only — the credit estimate (list-gen price plus the enrichment's upper bound when auto_run_enrichment is true); nothing was started
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EstimateCostResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /enrichments:
    post:
      summary: create enrichment
      description: |
        Create an enrichment without running it. Two bodies:

        ## From a completed list build

        Available on every account. `{ "dataset_id": "ds_..." }`, the id of a completed list build ([POST /datasets](#tag/list-builds/post/datasets) with `max_items`): the build's rows become the enrichment's input rows and its attributes become output columns. Configure further in the UI or via [PATCH /enrichments/{id}](#tag/enrich/patch/enrichments/{id}), then run with [POST /enrich](#tag/enrich/post/enrich). The response is `{ id, name, app_url, dataset_id }`.

        ## Save a recipe (beta)

        Without `dataset_id`: the task `prompt` the row agent reads for every row, the `output` columns it fills, and optionally `input_columns` (the headers rows must carry; empty = any), a `budget` in credits per row (default 1.5 per output column; it then moves by 1.5 as columns are added or removed until `budget_locked` is true), and the tools (`only_tools` replaces the default set, `additional_tools` adds to it; names from [GET /tools](#tag/datasets-beta/get/tools)). Each `output` entry is a column name or `{ "name", "prompt"?, "output_schema"? }`; `output_schema` is a JSON Schema for the cell value (see `help` → `output_schema`); without one, a column whose value is one plain value (an email, URL, date, number, boolean) is typed from the prompts. Output names may not repeat an input column. The response is the [enrichment](#model/enrichment).

        Run it on rows you send with [POST /enrich](#tag/enrich/post/enrich) (`enrichment_id` + `input`), or bind it to a dataset with [POST /datasets](#tag/datasets-beta/post/datasets) (`enrichment_id`) and fill the dataset. On an account without the datasets beta this body answers `403 feature_not_enabled`.
      operationId: createEnrichment
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const created = await riveter.enrichments.create({
              dataset_id: "ds_YOUR_DATASET_ID",
            });
            console.log(created.id); // enr_...
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            created = riveter.enrichments.create(dataset_id="ds_YOUR_DATASET_ID")
            print(created.id)  # enr_...
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            created = riveter.enrichments.create(dataset_id: "ds_YOUR_DATASET_ID")
            puts created.id # enr_...
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            created, err := client.Enrichments.Create(context.Background(), "ds_YOUR_DATASET_ID")
            fmt.Println(created.ID) // enr_...
      x-mcp-description: |
        Create an enrichment (no run, free). Either `dataset_id` of a completed list build (on every account): its rows and attributes become the enrichment; returns `{ id, name, app_url }`. Or a recipe (beta per account, else 403 `feature_not_enabled`): `prompt` (the task), `output` columns (names, or `{ name, prompt, output_schema }`; `help` with `output_schema`), optional `input_columns`, `budget` (credits per row, default 1.5 per output column; follows column adds / removes unless `budget_locked`), `only_tools` / `additional_tools` (`list_tools`). Then `enrich` with `enrichment_id` + `input`, or `build_dataset` with `enrichment_id`.
      tags:
        - Enrich
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                dataset_id:
                  type: string
                  description: "From a list build: the id of a completed list build (ds_...). Not with the fields below."
                name:
                  type: string
                  maxLength: 200
                  description: "Recipe (beta): display name"
                prompt:
                  type: string
                  maxLength: 50000
                  description: "Recipe (beta): the task prompt the row agent reads for every row. Required without dataset_id."
                input_columns:
                  type: array
                  items:
                    type: string
                  maxItems: 50
                  description: "Recipe (beta): the headers rows must carry. Omit for any."
                output:
                  type: array
                  items:
                    $ref: "#/components/schemas/OutputColumnSpec"
                  maxItems: 50
                  description: "Recipe (beta): the columns to fill; a plain string is a name whose type is inferred. Required without dataset_id."
                budget:
                  type: number
                  minimum: 0.1
                  maximum: 100
                  description: "Recipe (beta): credits the row agent may spend per row. Default 1.5 per output column."
                budget_locked:
                  type: boolean
                  default: false
                  description: "Recipe (beta): true keeps the budget as set when output columns are later added or removed; false lets it move by 1.5 per column."
                only_tools:
                  type: array
                  items:
                    type: string
                  description: "Recipe (beta): exactly these tools (GET /tools). Replaces the default set."
                additional_tools:
                  type: array
                  items:
                    type: string
                  description: "Recipe (beta): the default tools plus these (GET /tools; some need enabling on the account or a linked key)."
            examples:
              from_list_build:
                summary: From a completed list build
                value:
                  dataset_id: "ds_018f6a70-1234-7abc-89ab-0123456789ab"
              company_research:
                summary: Save a recipe (beta) — three columns, one typed
                value:
                  name: "Company basics"
                  prompt: "Research each company from its name and website."
                  input_columns: ["Company", "Website"]
                  output:
                    - "CEO"
                    - name: "Employee count"
                      prompt: "Current headcount as an integer"
                      output_schema: { type: integer, minimum: 0 }
                    - name: "Summary"
                      prompt: "Two sentences on what the company sells and to whom"
                  budget: 4
      responses:
        "201":
          description: From a list build — `{ id, name, app_url, dataset_id }`. Save a recipe — the saved enrichment
          content:
            application/json:
              schema:
                oneOf:
                  - title: From a list build
                    type: object
                    properties:
                      id:
                        type: string
                        description: The new enrichment's id (enr_...)
                      name:
                        type: string
                      app_url:
                        type: string
                        format: uri
                      dataset_id:
                        type: string
                  - $ref: "#/components/schemas/Enrichment"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
    get:
      summary: list enrichments
      description: |
        List the account's enrichments, most recently updated first: the enrichments built in the app or from a list build, and the recipes saved with [POST /enrichments](#tag/enrich/post/enrichments) (`prompt` + `output`, beta). Each item is compact — id, name, timestamps, and column names/count — so the list stays small even for accounts with thousands of enrichments. Column configuration (prompts, tools, formats) is on [GET /enrichments/{id}](#tag/enrich/get/enrichments/{id}).

        `status` and `app_url` are present on the app-built enrichments only; a saved recipe has neither. Filter by `status` (comma-separated; leaves the recipes out) and `name` (case-insensitive substring). Paginate with `page` / `per_page` (max 50). Column names are capped at 40 per input/output list; when cut, `column_names_truncated` is `true` and `column_count` still reports the total.
      operationId: listEnrichments
      x-mcp-description: |
        The account's enrichments, newest first: id, name, input / output column names (app-built ones also `status`, `app_url`). Paginate with `page` / `per_page` (max 50); filter by `status` (app-built only), `name`. `get_enrichment` has the columns' config.
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const page = await riveter.enrichments.list({ name: "contact" });
            for await (const enrichment of page) {
              console.log(enrichment.id, enrichment.name, enrichment.input_columns);
            }
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            page = riveter.enrichments.list(name="contact")
            for enrichment in page.auto_paging_iter():
                print(enrichment.id, enrichment.name, enrichment.input_columns)
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            riveter.enrichments.list(name: "contact").auto_paging_each do |enrichment|
              puts "#{enrichment.id} #{enrichment.name} #{enrichment.input_columns}"
            end
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            page, err := client.Enrichments.List(context.Background(), &riveter.ListEnrichmentsParams{Name: "contact"})
            for _, enrichment := range page.Enrichments {
                fmt.Println(enrichment.ID, enrichment.Name, enrichment.InputColumns)
            }
      tags:
        - Enrich
      parameters:
        - name: status
          in: query
          required: false
          description: "Comma-separated: pending, enqueued, processing, success, stopped"
          schema:
            type: string
        - name: name
          in: query
          required: false
          description: Case-insensitive substring match on the enrichment name
          schema:
            type: string
        - name: page
          in: query
          required: false
          schema:
            type: integer
            default: 1
        - name: per_page
          in: query
          required: false
          schema:
            type: integer
            default: 25
            maximum: 50
      responses:
        "200":
          description: Enrichments listed
          content:
            application/json:
              schema:
                type: object
                properties:
                  enrichments:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          description: Enrichment id (enr_...)
                        name:
                          type: string
                        status:
                          type: string
                          enum:
                            [pending, enqueued, processing, success, stopped]
                          description: App-built enrichments only
                        app_url:
                          type: string
                          format: uri
                          description: App-built enrichments only
                        column_count:
                          type: integer
                          description: Total number of columns (input + output)
                        input_columns:
                          type: array
                          description: Source-data column headers — the keys `input` must use when running this enrichment (first 40)
                          items:
                            type: string
                        output_columns:
                          type: array
                          description: Output column headers (first 40)
                          items:
                            type: string
                        column_names_truncated:
                          type: boolean
                          description: Present and true only when a column-name list was cut at 40 entries
                        created_at:
                          type: string
                          format: date-time
                        updated_at:
                          type: string
                          format: date-time
                  pagination:
                    $ref: "#/components/schemas/Pagination"
              example:
                enrichments:
                  - id: enr_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b
                    name: Deal accounting contact finder
                    status: success
                    app_url: https://app.riveterhq.com/enrichments/enr_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b
                    column_count: 3
                    input_columns: [Company]
                    output_columns: [CFO name, CFO email]
                    created_at: "2026-08-01T12:00:00Z"
                    updated_at: "2026-09-01T09:30:00Z"
                  - id: enr_018f5b60-1234-7abc-89ab-0123456789ab
                    name: Company basics
                    column_count: 5
                    input_columns: [Company, Website]
                    output_columns: [CEO, Employee count, Summary]
                    created_at: "2026-10-01T12:00:00Z"
                    updated_at: "2026-10-01T12:00:00Z"
                pagination:
                  page: 1
                  per_page: 25
                  total_count: 2
                  total_pages: 1
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /enrichments/summary:
    get:
      summary: list enrichments (summary)
      description: |
        All-time enrichment counts by status — how many of the account's enrichments are pending, running, finished, or stopped. For the enrichments themselves use [GET /enrichments](#tag/enrich/get/enrichments)`?status=...`.
      operationId: enrichmentsSummary
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const summary = await riveter.enrichments.summary();
            console.log(summary.counts);
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            summary = riveter.enrichments.summary()
            print(summary.counts)
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            summary = riveter.enrichments.summary
            puts summary.counts
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            summary, err := client.Enrichments.Summary(context.Background())
            fmt.Printf("%+v\n", summary.Counts)
      tags:
        - Enrich
      responses:
        "200":
          description: Enrichment counts by status
          content:
            application/json:
              schema:
                type: object
                properties:
                  counts:
                    type: object
                    properties:
                      pending:
                        type: integer
                      enqueued:
                        type: integer
                      processing:
                        type: integer
                      success:
                        type: integer
                      stopped:
                        type: integer
              example:
                counts:
                  pending: 12
                  enqueued: 0
                  processing: 1
                  success: 340
                  stopped: 6
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /datasets:
    post:
      summary: build dataset
      description: |
        Create a dataset — or, with a list-build body, run the list builder (every account; see "List build" at the end).

        ## What a dataset is

        A dataset is a table in Riveter. Its **input columns** are your starting data: a company name, a URL, a product identifier, etc. Its **output columns** hold what Riveter finds out about each row: the CEO, data from a page, product images, etc. The output columns, the instructions for filling them, and the tools allowed are an **enrichment**, and a dataset belongs to one (`enrichment_id`). Creating a dataset and adding rows is free. Filling the cells is a run of the enrichment, [POST /enrich](#tag/enrich/post/enrich) with the `dataset_id`, and that run spends credits: one AI agent per row.

        Rows are deduplicated on the first input column (`index_column`; change it with [PATCH /datasets/{id}](#tag/datasets-beta/patch/datasets/{id})). Every row has an id (`row_...`) and a position counted from 1.

        **Beta.** The dataset endpoints are turned on per account. On an account without them, every endpoint in this section answers `403` with `type: "feature_not_enabled"`; email [support@riveterhq.com](mailto:support@riveterhq.com) to turn them on. The request and response shapes may still change during the beta. (Email support@rivetehq.com if you would like this enabled).

        ## 1. Build a dataset from an enrichment you already have

        The enrichment holds the columns, so the request is its id plus the rows. Take the `enrichment_id` from a dataset in the app (the dataset page's **API** button shows it), from [GET /datasets/{id}](#tag/datasets-beta/get/datasets/{id}), or from [GET /enrichments](#tag/enrich/get/enrichments); or save a new one with [POST /enrichments](#tag/enrich/post/enrichments). Rows are objects keyed by the enrichment's input column names ([GET /enrichments/{id}](#tag/enrich/get/enrichments/{id}) lists them), up to 100 per request; add more with [POST /datasets/{id}/rows](#tag/datasets-beta/post/datasets/{id}/rows) or a CSV on [POST /datasets/{id}/rows/import](#tag/datasets-beta/post/datasets/{id}/rows/import).

        ```bash
        curl -X POST "https://api.riveterhq.com/v1/datasets" \
          -H "Authorization: Bearer YOUR_API_KEY" \
          -H "Content-Type: application/json" \
          -d '{
            "name": "Q4 targets",
            "enrichment_id": "enr_YOUR_ENRICHMENT_ID",
            "rows": [
              { "Company": "Apple", "Website": "apple.com" },
              { "Company": "Shopify", "Website": "shopify.com" }
            ]
          }'
        ```

        The response is the dataset: `id` (`ds_...`), `input_columns`, `enrichment_id`, `output_columns`, `row_count`. Nothing has run yet. Fill the cells:

        ```bash
        curl -X POST "https://api.riveterhq.com/v1/enrich" \
          -H "Authorization: Bearer YOUR_API_KEY" \
          -H "Content-Type: application/json" \
          -d '{ "dataset_id": "ds_YOUR_DATASET_ID" }'
        ```

        This fills every empty cell and returns a run (`id`: `run_...`). Add `"estimate_cost": true` to see the price without running anything, or `"max_credits": 50` to refuse the run above a ceiling. Then read the results:

        ```bash
        curl "https://api.riveterhq.com/v1/runs/RUN_ID/result?wait=50" \
          -H "Authorization: Bearer YOUR_API_KEY"
        ```

        `wait` holds the request for up to 50 seconds. `output` is `null` until the run finishes, so call again until it is not; or pass `webhook_url` on the fill and skip polling. `output.rows` is the rows the fill ran, each with its `input`, its `cells` by output column name (`value`, `state`, `sources`), and the `credits` it cost. [GET /datasets/{id}/rows](#tag/datasets-beta/get/datasets/{id}/rows) is the whole grid at any time, filled cells included.

        ## 2. Large datasets

        `output.rows` holds up to 5,000 rows. Above that, `output` is `null` and the run's `output_files` are the result: `output_files.jsonl` and `output_files.csv` are the whole grid as files on object storage, one row per line. Each has a `state` (`pending` until the export lands, shortly after the run finishes; then `ready`), a signed `url`, and `expires_at` (one hour; fetch the result again for a fresh link).

        ```bash
        curl -o rows.jsonl "$(curl -s "https://api.riveterhq.com/v1/runs/RUN_ID/result" \
          -H "Authorization: Bearer YOUR_API_KEY" | jq -r '.output_files.jsonl.url')"
        ```

        Two more ways to read a large grid: page [GET /datasets/{id}/rows](#tag/datasets-beta/get/datasets/{id}/rows) with `limit` (up to 1,000) and `cursor`, or `updated_since` for only the rows changed since a time; or download the grid at any time with [GET /datasets/{id}/export](#tag/datasets-beta/get/datasets/{id}/export) (`output_format` `jsonl`, `csv`, or `xlsx`).

        ## 3. Build a dataset from a prompt

        No rows yet? Describe them. Riveter's list generator finds `count` rows for the `prompt`; `input_columns` names the identity columns to generate. The output columns come from `attributes` (an enrichment is created for the dataset from the prompt and the column names) or from an `enrichment_id`. `auto_fill: true` fills the cells as soon as the rows land. Generation and the fill are both paid runs; `estimate_cost: true` prices them first.

        ```bash
        curl -X POST "https://api.riveterhq.com/v1/datasets" \
          -H "Authorization: Bearer YOUR_API_KEY" \
          -H "Content-Type: application/json" \
          -d '{
            "name": "SF coffee roasters",
            "prompt": "Specialty coffee roasters in San Francisco",
            "count": 50,
            "input_columns": ["Roaster", "Website"],
            "attributes": ["Founded year", "Number of cafes"],
            "auto_fill": true
          }'
        ```

        The response is the dataset plus `run`, the generation run; poll it like any run. The rows land in the dataset when generation finishes, and with `auto_fill` the fill run starts then: it appears under the dataset's `runs` on [GET /datasets/{id}](#tag/datasets-beta/get/datasets/{id}), and `fill_webhook_url` is POSTed when it finishes. Read the grid as in step 2. For more rows later, [POST /datasets/{id}/rows/build](#tag/datasets-beta/post/datasets/{id}/rows/build).

        ## List build (every account)

        The same path without a dataset: `prompt` and / or `identifiers` + `qualifiers` with `max_items` runs the list builder. It finds rows for a description and returns them on the run ([GET /runs/{id}/result](#tag/runs/get/runs/{id}/result)); nothing is kept as a dataset. `attributes` are not filled by the build; they become the output columns of an enrichment that runs later (`auto_run_enrichment: true`, or [POST /enrich](#tag/enrich/post/enrich) with the `dataset_id` the run returns). "Build a list" in the app; see the [List builds](#tag/list-builds) section.

        **Which mode runs.** `rows` or `count` makes the request a dataset (so do `input_columns`, `enrichment_id`, `auto_fill`, `name`, `webhook_url`, and `fill_webhook_url`); `max_items`, `identifiers`, `qualifiers`, and `dataset_webhook_url` make it a list build.
      operationId: buildDataset
      x-mcp-open-world: true
      x-mcp-description: |
        Build a dataset: rows Riveter finds, or rows you send. The body picks the mode.
        List build (on every account): `prompt` and / or `identifiers` + `qualifiers`, `max_items` (row cap); `attributes` (≤ 20) are saved for a later enrichment, not filled (`auto_run_enrichment: true` fills them, a second paid run). Returns a run plus `dataset_id`; then `enrich` with that `dataset_id` and a column config. `help` with `datasets_vs_enrichment`.
        Dataset (beta per account, else 403 `feature_not_enabled`): `rows` (≤ 100 objects keyed by input column name, free) or `prompt` + `count` (generated, paid); output columns from `attributes` or `enrichment_id`; `auto_fill: true` fills generated rows when they land. Returns the dataset (`id`, `ds_...`) and `run` (the generation run, or null); then `enrich` with `dataset_id` alone. `help` with `datasets`.
        `estimate_cost: true` prices either mode first.
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const run = await riveter.datasets.build({
              prompt: "Top 100 US fintech startups",
              max_items: 100,
            });
            const result = await riveter.runs.waitForResult(run.id);
            console.log(result.output);
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            run = riveter.datasets.build(
                prompt="Top 100 US fintech startups",
                max_items=100,
            )
            result = riveter.runs.wait_for_result(run.id)
            print(result.output)
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            run = riveter.datasets.build(
              prompt: "Top 100 US fintech startups",
              max_items: 100
            )
            result = riveter.runs.wait_for_result(run.id)
            puts result.output
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            ctx := context.Background()
            run, err := client.Datasets.Build(ctx, riveter.BuildDatasetParams{
                Prompt:   "Top 100 US fintech startups",
                MaxItems: 100,
            })
            result, err := client.Runs.WaitForResult(ctx, run.ID, nil)
            fmt.Println(string(result.Output))
      tags:
        - List builds
        - Datasets (beta)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                prompt:
                  type: string
                  description: "What the rows are. List build: required unless identifiers are given. Dataset: requires count."
                identifiers:
                  type: array
                  items:
                    type: string
                  maxItems: 3
                  description: 'List build: what each row is (e.g. ["Company name", "Website"]). Max 3.'
                qualifiers:
                  type: array
                  items:
                    type: string
                  maxItems: 10
                  description: "List build: constraints each row must satisfy. Max 10."
                max_items:
                  type: integer
                  description: "List build: max rows to generate (capped by your plan)"
                attributes:
                  type: array
                  items:
                    type: string
                  maxItems: 50
                  description: "Output column names, not filled by the build. List build: max 20, filled by auto_run_enrichment or a later POST /enrich. Dataset: become a private enrichment's columns, typed from the prompts; not with enrichment_id."
                dataset_webhook_url:
                  type: string
                  format: uri
                  description: "List build: URL to POST the dataset results to when the build completes"
                auto_run_enrichment:
                  type: boolean
                  default: false
                  description: "List build: when the build completes, run an enrichment of the attributes on the rows. A second paid run; its id is enrichment_run_id in the response."
                auto_run_enrichment_webhook_url:
                  type: string
                  format: uri
                  description: "List build: webhook for the auto-run enrichment results"
                name:
                  type: string
                  maxLength: 200
                  description: "Dataset: display name"
                input_columns:
                  type: array
                  items:
                    type: string
                  maxItems: 50
                  description: "Dataset: input column names (row identity). Optional with rows."
                rows:
                  type: array
                  items:
                    type: object
                    additionalProperties:
                      type: string
                  maxItems: 100
                  description: "Dataset: the rows, keyed by input column name. Up to 100; more with POST /datasets/{id}/rows/import."
                count:
                  type: integer
                  minimum: 1
                  maximum: 12000
                  description: "Dataset: how many rows to generate for prompt. Capped per account."
                enrichment_id:
                  type: string
                  description: "Dataset: a saved enrichment (enr_...) whose output columns become the dataset's. Not with attributes."
                auto_fill:
                  type: boolean
                  default: false
                  description: "Dataset, with count: fill the output columns when the rows land (a second paid run)."
                webhook_url:
                  type: string
                  format: uri
                  description: "Dataset: POSTed the list-generation run when it finishes"
                fill_webhook_url:
                  type: string
                  format: uri
                  description: "Dataset, with auto_fill: POSTed the fill run when it finishes"
                estimate_cost:
                  $ref: "#/components/schemas/EstimateCostParam"
                max_credits:
                  $ref: "#/components/schemas/MaxCreditsParam"
            examples:
              prompt_with_spec:
                summary: List build — prompt with identifiers and qualifiers
                value:
                  prompt: "Top 50 US SaaS companies with their websites"
                  identifiers: ["Company name", "Website"]
                  qualifiers: ["US-based", "SaaS"]
                  max_items: 50
              build_and_enrich:
                summary: List build — build and auto-enrich in one step, refused above 200 credits
                value:
                  prompt: "Top 50 US SaaS companies"
                  attributes: ["CEO", "Revenue"]
                  max_items: 50
                  auto_run_enrichment: true
                  max_credits: 200
              rows_and_attributes:
                summary: Dataset (beta) — rows you have, two columns to fill
                value:
                  name: "Targets"
                  rows:
                    - { "Company": "Apple", "Website": "apple.com" }
                    - { "Company": "Shopify" }
                  attributes: ["CEO", "HQ city"]
              generated_rows_auto_fill:
                summary: Dataset (beta) — generate 50 rows and fill them when they land
                value:
                  prompt: "Specialty coffee roasters in San Francisco"
                  count: 50
                  input_columns: ["Roaster", "Website"]
                  attributes: ["Founded year"]
                  auto_fill: true
      responses:
        "201":
          description: List build — the run plus `dataset_id` / `max_items` (and `enrichment_run_id` when auto-running). Dataset — the dataset, with `run` when list generation started (null when the rows came in the request)
          content:
            application/json:
              schema:
                oneOf:
                  - title: List build run
                    allOf:
                      - $ref: "#/components/schemas/Run"
                      - type: object
                        properties:
                          dataset_id:
                            type: string
                          max_items:
                            type: integer
                          enrichment_run_id:
                            type: string
                            description: Present when auto_run_enrichment is true
                  - title: Dataset
                    allOf:
                      - $ref: "#/components/schemas/Dataset"
                      - type: object
                        properties:
                          run:
                            $ref: "#/components/schemas/Run"
        "200":
          description: estimate_cost only — the credit estimate (list build — the list-gen price on max_items, plus attributes × max_items with auto_run_enrichment; dataset — list generation on count, plus every row's budget with auto_fill); nothing was started
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EstimateCostResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
    get:
      summary: list datasets
      description: |
        The account's datasets, most recently updated first (up to 200). Each item is compact: id, name, column counts, `row_count`, and the newest run. Columns, the bound enrichment, and recent runs are on [GET /datasets/{id}](#tag/datasets-beta/get/datasets/{id}); rows on [GET /datasets/{id}/rows](#tag/datasets-beta/get/datasets/{id}/rows).
      operationId: listDatasets
      x-badges:
        - name: Beta
          color: "#e9d5ff"
      x-mcp-description: |
        Datasets beta, per account (else 403 `feature_not_enabled`). The account's datasets, newest first (up to 200): id, name, column counts, `row_count`, newest run. `get_dataset` has the columns and enrichment.
      tags:
        - Datasets (beta)
      responses:
        "200":
          description: Datasets listed
          content:
            application/json:
              schema:
                type: object
                properties:
                  datasets:
                    type: array
                    items:
                      $ref: "#/components/schemas/DatasetListItem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /datasets/{id}:
    get:
      summary: read dataset
      description: |
        One dataset: `input_columns`, `index_column`, the bound `enrichment_id` and its `output_columns` (names; prompts and schemas are on [GET /enrichments/{id}](#tag/enrich/get/enrichments/{id})), `row_count`, whether the rows expired (`rows_expired`), whether a CSV import is still landing (`import_in_progress`), and the five newest `runs` (list generation and fills).
      operationId: getDataset
      x-badges:
        - name: Beta
          color: "#e9d5ff"
      x-mcp-description: |
        Datasets beta, per account (else 403 `feature_not_enabled`). One dataset by id: `input_columns`, `index_column`, `enrichment_id` + `output_columns` (names; `get_enrichment` has the prompts), `row_count`, `import_in_progress`, newest `runs`. Rows are `list_dataset_rows`.
      tags:
        - Datasets (beta)
      parameters:
        - $ref: "#/components/parameters/DatasetId"
      responses:
        "200":
          description: The dataset
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Dataset"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      summary: update dataset
      description: |
        Edit a dataset's structure: `name`, `index_column`, `input_columns` (add, rename, delete), `output` columns (add, change the prompt or schema, rename, delete), and the per-row `budget` / `budget_locked` of its enrichment. Nothing runs; fill with [POST /enrich](#tag/enrich/post/enrich). Changing a column's prompt or schema marks its filled cells `stale`. A saved or shared enrichment is copied before the edit, so `enrichment_id` may change. Refused with 409 `dataset_in_use` while a run is on the dataset.

        The budget follows the columns as on [PATCH /enrichments/{id}](#tag/datasets-beta/patch/enrichments/{id}): adding an output column raises `budget_credits_per_row` by 1.5, removing one lowers it by 1.5, until `budget_locked` is true. `budget_locked: true` or `budget` sent in the same request as column changes wins over that adjustment, so one call can lock the budget and add columns. Both need an output column to land on: on a dataset without one, send them with the first `output` add.
      operationId: updateDataset
      x-badges:
        - name: Beta
          color: "#e9d5ff"
      x-mcp-destructive: true
      x-mcp-description: |
        Datasets beta, per account (else 403 `feature_not_enabled`). Edit a dataset's structure (no run): `name`; `index_column`; `input_columns` ops (`{ "Name": {} }` add, `{ "rename" }`, `{ "delete": true }`); `output` column ops (new name adds, type inferred unless `output_schema` is given; `{ "prompt", "output_schema" }` changes, cells go `stale`; `{ "rename" }`; `{ "delete": true }`); `budget` (credits per row; moves by 1.5 per output column added or removed unless `budget_locked`) and `budget_locked` (true / false; sent with column changes it wins over that adjustment). Edits a shared enrichment on a private copy. 409 `dataset_in_use` while a fill runs. Then `enrich` with `dataset_id`. `help` with `datasets`, `output_schema`.
      tags:
        - Datasets (beta)
      parameters:
        - $ref: "#/components/parameters/DatasetId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 200
                index_column:
                  type: string
                  description: The input column rows are unique on (by name). Every row is re-keyed.
                input_columns:
                  $ref: "#/components/schemas/InputColumnOperations"
                output:
                  $ref: "#/components/schemas/OutputColumnOperations"
                budget:
                  type: number
                  minimum: 0.1
                  maximum: 100
                  description: Credits the row agent may spend per row, on the dataset's enrichment. Sent with column changes, it wins over the per-column adjustment.
                budget_locked:
                  type: boolean
                  description: true keeps the budget as set when output columns are added or removed; false lets it move by 1.5 per column again.
            examples:
              change_columns:
                summary: Change a prompt, add a typed column, remove one
                value:
                  output:
                    "CEO":
                      prompt: "Full name of the current CEO"
                    "Employee count":
                      prompt: "Current headcount as an integer"
                      output_schema: { type: integer, minimum: 0 }
                    "HQ city":
                      delete: true
              lock_budget_and_add_columns:
                summary: Lock the budget and add columns without moving it
                value:
                  budget_locked: true
                  output:
                    "Founded": {}
                    "Revenue":
                      prompt: "Latest annual revenue in USD"
              add_input_column:
                summary: Add an input column and dedupe on it
                value:
                  input_columns:
                    "Ticker": {}
                  index_column: "Ticker"
              rename:
                summary: Rename the dataset and a column
                value:
                  name: "Q4 targets"
                  input_columns:
                    "Website":
                      rename: "Domain"
      responses:
        "200":
          description: The dataset after the edit
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Dataset"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
    delete:
      summary: delete dataset
      description: |
        Delete a dataset and its rows. Its enrichment is kept. Refused with 409 `dataset_in_use` while a build or fill runs on it.
      operationId: deleteDataset
      x-badges:
        - name: Beta
          color: "#e9d5ff"
      x-mcp-destructive: true
      x-mcp-description: |
        Datasets beta, per account (else 403 `feature_not_enabled`). Delete a dataset by id (rows and cells with it; its enrichment stays). 409 `dataset_in_use` while a run is on it.
      tags:
        - Datasets (beta)
      parameters:
        - $ref: "#/components/parameters/DatasetId"
      responses:
        "204":
          description: Deleted
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"

  /datasets/{id}/rows:
    get:
      summary: list rows
      description: |
        The dataset's rows in position order, each with its `input` values, its `cells` by output column name (`value`, `state`, `sources`), and the `credits` spent on it. Page with `limit` (max 1,000) and `cursor`; `updated_since` returns only rows changed since then.
      operationId: listDatasetRows
      x-badges:
        - name: Beta
          color: "#e9d5ff"
      x-mcp-description: |
        Datasets beta, per account (else 403 `feature_not_enabled`). Rows of a dataset in position order: `input` by column name, `cells` by output column name (`value`, `state`: pending / running / ok / not_found / failed / manual / stale, `reason` + `error` on failed), `credits` and `not_found_reason` per row. `limit` up to 1,000; pass `next_cursor` back as `cursor` for the next page; `updated_since` for changes only.
      tags:
        - Datasets (beta)
      parameters:
        - $ref: "#/components/parameters/DatasetId"
        - name: limit
          in: query
          schema:
            type: integer
            default: 100
            minimum: 1
            maximum: 1000
        - name: cursor
          in: query
          description: The `next_cursor` of the previous page
          schema:
            type: integer
        - name: updated_since
          in: query
          description: Only rows updated at or after this time (ISO 8601)
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: One page of rows
          content:
            application/json:
              schema:
                type: object
                properties:
                  rows:
                    type: array
                    items:
                      $ref: "#/components/schemas/DatasetRow"
                  next_cursor:
                    type: integer
                    nullable: true
                    description: Pass back as `cursor` for the next page; null on the last page
                  row_count:
                    type: integer
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      summary: add rows
      description: |
        Append up to 100 rows, each an object keyed by input column name (missing columns are empty). Free. Rows that duplicate an existing one on the index column are skipped and counted in `skipped_duplicates`. New rows' cells are `pending`; fill them with [POST /enrich](#tag/enrich/post/enrich) `{ "dataset_id": ... }`. More than 100 rows: [POST /datasets/{id}/rows/import](#tag/datasets-beta/post/datasets/{id}/rows/import).
      operationId: addDatasetRows
      x-badges:
        - name: Beta
          color: "#e9d5ff"
      x-mcp-description: |
        Datasets beta, per account (else 403 `feature_not_enabled`). Append up to 100 rows (objects keyed by input column name; free). Duplicates on the index column are skipped (`skipped_duplicates`). Returns the inserted rows. Then `enrich` with `dataset_id`. More rows: `import_dataset_rows`.
      tags:
        - Datasets (beta)
      parameters:
        - $ref: "#/components/parameters/DatasetId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                rows:
                  type: array
                  items:
                    type: object
                    additionalProperties:
                      type: string
                  maxItems: 100
                  description: Objects keyed by input column name
              required:
                - rows
            example:
              rows:
                - { "Company": "Stripe", "Website": "stripe.com" }
                - { "Company": "Plaid" }
      responses:
        "201":
          description: The rows inserted
          content:
            application/json:
              schema:
                type: object
                properties:
                  rows:
                    type: array
                    items:
                      $ref: "#/components/schemas/DatasetRow"
                  skipped_duplicates:
                    type: integer
                  row_count:
                    type: integer
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
    delete:
      summary: delete rows
      description: |
        Remove up to 100 rows by id (`ids`, comma-separated `row_...`) and / or by position (`positions`, comma-separated integers from 1). Positions of the other rows do not change. Unknown ids or positions are a 422; nothing is deleted then.
      operationId: deleteDatasetRows
      x-badges:
        - name: Beta
          color: "#e9d5ff"
      x-mcp-destructive: true
      x-mcp-description: |
        Datasets beta, per account (else 403 `feature_not_enabled`). Remove rows: `ids` (comma-separated `row_...` ids) and / or `positions` (comma-separated positions from 1), up to 100 rows together. Returns the number deleted.
      tags:
        - Datasets (beta)
      parameters:
        - $ref: "#/components/parameters/DatasetId"
        - name: ids
          in: query
          required: false
          description: Comma-separated row ids (row_...). ids and positions together name up to 100 rows; one of them is required
          schema:
            type: string
        - name: positions
          in: query
          required: false
          description: Comma-separated row positions (integers from 1, as GET rows reports them)
          schema:
            type: string
      responses:
        "200":
          description: Rows deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: integer
                  row_count:
                    type: integer
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /datasets/{id}/rows/import:
    post:
      summary: import rows (CSV)
      description: |
        Append rows from a CSV whose header row holds column names. Input column values become the row's input; output column values land as `manual` cells. Send the CSV as the body (`text/csv`), as a multipart `file`, or as the `csv` string of a JSON body (2 MB max). Up to 1,000 rows land at once (`201`); a bigger file is imported in the background (`202`; [GET /datasets/{id}](#tag/datasets-beta/get/datasets/{id}) shows `import_in_progress`). Duplicates on the index column are skipped.
      operationId: importDatasetRows
      x-badges:
        - name: Beta
          color: "#e9d5ff"
      x-mcp-description: |
        Datasets beta, per account (else 403 `feature_not_enabled`). Append rows from CSV text (`csv`; header row = input and / or output column names, output values land as `manual` cells; free). Up to 1,000 rows land at once (`import.status` done); more are accepted and land in the background (`importing`; `get_dataset` shows `import_in_progress`). Duplicates on the index column are skipped.
      tags:
        - Datasets (beta)
      parameters:
        - $ref: "#/components/parameters/DatasetId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                csv:
                  type: string
                  description: The CSV text. First row = input and / or output column names.
              required:
                - csv
            example:
              csv: "Company,Website\nStripe,stripe.com\nPlaid,plaid.com\n"
          text/csv:
            schema:
              type: string
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
      responses:
        "201":
          description: The rows landed in this request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DatasetImportResult"
        "202":
          description: The file was accepted and is landing from a job (`import.status` is `importing`)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DatasetImportResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "413":
          description: The body is over the 2 MB limit
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /datasets/{id}/rows/build:
    post:
      summary: generate more rows
      description: |
        Ask the list generator for `count` more rows of an existing dataset. Requires a `index_column` on the dataset (set one with [PATCH /datasets/{id}](#tag/datasets-beta/patch/datasets/{id})); the generator is told the existing rows and skips them, and anything it still repeats is dropped on the index column. `prompt` says what to find; when omitted, the prompt of the dataset's latest build is reused. The generator may find fewer rows than `count`: when the run finishes, its `progress` reports `requested_rows`, `generated_rows`, `added_rows`, `skipped_duplicate_rows`. `auto_fill: true` fills the new rows' output columns when they land (a second paid run). The response is the dataset plus `run` (the generation run); poll it with [GET /runs/{id}](#tag/runs/get/runs/{id}) or pass `webhook_url`.
      operationId: buildDatasetRows
      x-badges:
        - name: Beta
          color: "#e9d5ff"
      x-mcp-open-world: true
      x-mcp-description: |
        Datasets beta, per account (else 403 `feature_not_enabled`). Generate `count` more rows for a dataset (paid; `estimate_cost` first). Needs a `index_column` on the dataset (`update_dataset`). `prompt` says what to find (default: the dataset's last build prompt); the generator skips the existing rows and may find fewer than `count` — the finished run's `progress` has `requested_rows`, `generated_rows`, `added_rows`, `skipped_duplicate_rows`. `auto_fill: true` fills them when they land. Returns the dataset and `run`.
      tags:
        - Datasets (beta)
      parameters:
        - $ref: "#/components/parameters/DatasetId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                prompt:
                  type: string
                  description: "What the rows are. Default: the prompt of the dataset's latest build."
                count:
                  type: integer
                  minimum: 1
                  maximum: 12000
                  description: How many rows to generate. Capped per account.
                auto_fill:
                  type: boolean
                  default: false
                  description: Fill the new rows' output columns when they land (needs an enrichment on the dataset)
                webhook_url:
                  type: string
                  format: uri
                  description: POSTed the generation run when it finishes
                fill_webhook_url:
                  type: string
                  format: uri
                  description: With auto_fill — POSTed the fill run when it finishes
                estimate_cost:
                  $ref: "#/components/schemas/EstimateCostParam"
                max_credits:
                  $ref: "#/components/schemas/MaxCreditsParam"
              required:
                - count
            examples:
              more_rows:
                summary: Fifty more, filled on arrival
                value:
                  prompt: "Specialty coffee roasters in Oakland and Berkeley"
                  count: 50
                  auto_fill: true
      responses:
        "201":
          description: The dataset with `run` (the generation run)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Dataset"
                  - type: object
                    properties:
                      run:
                        $ref: "#/components/schemas/Run"
        "200":
          description: estimate_cost only — the credit estimate; nothing was started
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EstimateCostResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /datasets/{id}/rows/{row_id}:
    patch:
      summary: set input values
      description: |
        Set a row's `input` values and / or `cells` by hand. Nothing runs. A cell set this way is `manual`: fills skip it unless `force`. `null` clears a value. Refused with 409 `row_in_use` while a fill is on the row.
      operationId: updateDatasetRow
      x-badges:
        - name: Beta
          color: "#e9d5ff"
      x-mcp-destructive: true
      x-mcp-description: |
        Datasets beta, per account (else 403 `feature_not_enabled`). Write one row (`row_id`: a `row_...` id or a position) by hand. `input` is `{ "<input column>": value | null }` (an index-column value another row holds is a 422). `cells` is `{ "<output column>": value | null }`: a value becomes a `manual` cell (kept by fills unless `force`); `null` clears it back to `pending`. One write for both. 409 `row_in_use` while a fill is on the row.
      tags:
        - Datasets (beta)
      parameters:
        - $ref: "#/components/parameters/DatasetId"
        - name: row_id
          in: path
          required: true
          description: The row id (row_...) or its position (an integer from 1)
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: At least one of `input` and `cells`
              properties:
                input:
                  type: object
                  description: Input column name → value, or null to clear
                  additionalProperties:
                    type: [string, "null"]
                    description: The new value, at most 10,000 characters; null clears the value
                cells:
                  type: object
                  description: Output column name → value, or null to clear
                  additionalProperties:
                    type: [string, "null"]
                    description: The new value; null clears the cell. (Over HTTP any JSON value is accepted, e.g. an object for a column with an output_schema.)
            example:
              input:
                "Website": "apple.com"
              cells:
                "CEO": "Tim Cook"
                "HQ city": null
      responses:
        "200":
          description: The row after the write
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DatasetRow"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /datasets/{id}/columns/promote:
    post:
      summary: convert output column to input
      description: |
        Turn an output column into an input column of the same name. Every row's value moves with it, so later columns and dedupe can key on it. Refused with 409 `dataset_in_use` while a fill runs; the enrichment's last output column cannot be converted.
      operationId: promoteDatasetColumn
      x-badges:
        - name: Beta
          color: "#e9d5ff"
      x-mcp-destructive: true
      x-mcp-description: |
        Datasets beta, per account (else 403 `feature_not_enabled`). Make a filled output column an input column of the same name (`column`); every row's value moves with it, so later columns and dedupe can key on it. Not undoable; 409 `dataset_in_use` while a fill runs.
      tags:
        - Datasets (beta)
      parameters:
        - $ref: "#/components/parameters/DatasetId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                column:
                  type: string
                  description: The output column name
              required:
                - column
            example:
              column: "Domain"
      responses:
        "200":
          description: The dataset after the conversion
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Dataset"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /datasets/{id}/export:
    get:
      summary: export dataset (CSV, Excel, or JSON Lines)
      description: |
        The whole grid: input columns, then the enrichment's output columns, one row per dataset row, in position order. CSV by default (structured cell values are JSON text); `output_format=xlsx` is the same grid as an Excel workbook (every cell text, cells over Excel's 32,767-character limit truncated); `output_format=jsonl` keeps values structured, one JSON object per line. Built in memory; for very large datasets prefer the result files of the fill run ([GET /runs/{id}/result](#tag/runs/get/runs/{id}/result)).
      operationId: exportDataset
      x-badges:
        - name: Beta
          color: "#e9d5ff"
      x-mcp-description: |
        Datasets beta, per account (else 403 `feature_not_enabled`). The dataset as CSV text (or JSON Lines with `output_format: "jsonl"`; `xlsx` is binary and meant for a browser): input columns, then output columns, one line per row. For a few rows prefer `list_dataset_rows` (it carries cell states).
      tags:
        - Datasets (beta)
      parameters:
        - $ref: "#/components/parameters/DatasetId"
        - name: output_format
          in: query
          required: false
          description: "`csv` (default), `xlsx`, or `jsonl`."
          schema:
            type: string
            enum: [csv, xlsx, jsonl]
            default: csv
      responses:
        "200":
          description: The CSV, workbook, or JSON Lines body
          content:
            text/csv:
              schema:
                type: string
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema:
                type: string
                format: binary
            application/jsonl:
              schema:
                type: string
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /datasets/{id}/extend:
    post:
      summary: extend list build
      description: |
        Generate **new rows** for an existing completed list build. The new rows are deduplicated against the source build's rows. Identifiers and attributes are inherited from the source and cannot be overridden; `qualifiers` and `max_items` may be replaced, and an optional `prompt` adds a new instruction.

        Like [POST /datasets](#tag/list-builds/post/datasets), this finds rows only (identifier columns). Inherited attributes are not researched; they are filled by `auto_run_enrichment: true` or a later [POST /enrich](#tag/enrich/post/enrich) with the new `dataset_id`.

        Returns a fresh list-build run (the source build is untouched).

        Takes a list build id only. For a dataset (beta) use [POST /datasets/{id}/rows/build](#tag/datasets-beta/post/datasets/{id}/rows/build).
      operationId: extendDataset
      x-mcp-open-world: true
      x-mcp-description: |
        More deduplicated rows for a completed list build (a build id from `build_dataset` with `max_items`, not a dataset). For a dataset use `build_dataset_rows`.
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const run = await riveter.datasets.extend("ds_YOUR_DATASET_ID", {
              max_items: 50,
            });
            const result = await riveter.runs.waitForResult(run.id);
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            run = riveter.datasets.extend("ds_YOUR_DATASET_ID", max_items=50)
            result = riveter.runs.wait_for_result(run.id)
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            run = riveter.datasets.extend_dataset("ds_YOUR_DATASET_ID", max_items: 50)
            result = riveter.runs.wait_for_result(run.id)
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            ctx := context.Background()
            run, err := client.Datasets.Extend(ctx, "ds_YOUR_DATASET_ID",
                riveter.ExtendDatasetParams{MaxItems: 50})
            result, err := client.Runs.WaitForResult(ctx, run.ID, nil)
      tags:
        - List builds
      parameters:
        - name: id
          in: path
          required: true
          description: Id of the source dataset build (ds_...)
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                prompt:
                  type: string
                  description: Optional new instruction (e.g. "more rows like these but in Europe")
                qualifiers:
                  type: array
                  items:
                    type: string
                  maxItems: 10
                  description: Optional replacement qualifiers (defaults to the source's). Max 10.
                max_items:
                  type: integer
                  description: Max new rows (defaults to the source's max_items)
                dataset_webhook_url:
                  type: string
                  format: uri
                auto_run_enrichment:
                  type: boolean
                  default: false
                auto_run_enrichment_webhook_url:
                  type: string
                  format: uri
                estimate_cost:
                  $ref: "#/components/schemas/EstimateCostParam"
                max_credits:
                  $ref: "#/components/schemas/MaxCreditsParam"
      responses:
        "201":
          description: Extension build started — the run plus dataset_id / source_dataset_id / max_items
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Run"
                  - type: object
                    properties:
                      dataset_id:
                        type: string
                        description: The new build's dataset id
                      source_dataset_id:
                        type: string
                      max_items:
                        type: integer
        "200":
          description: estimate_cost only — the credit estimate; nothing was started
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EstimateCostResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /configured_datasets/{id}/build:
    post:
      summary: build configured list
      description: |
        Run a **configured list build** — a reusable, pre-configured list template (id `cds_...`) set up for your account ("Configured list builds" in the app). The spec, prompt template, row cap, and per-run credit cost are all defined on the template; you only supply `parameters` to fill in its `{{ placeholder }}` values.

        Returns the dataset-build run; poll [GET /runs/{id}](#tag/runs/get/runs/{id}).
      operationId: buildConfiguredDataset
      x-mcp-open-world: true
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const run = await riveter.configuredDatasets.build("cds_YOUR_CONFIGURED_DATASET_ID", {
              parameters: { City: "Denver" },
            });
            const result = await riveter.runs.waitForResult(run.id);
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            run = riveter.configured_datasets.build(
                "cds_YOUR_CONFIGURED_DATASET_ID",
                parameters={"City": "Denver"},
            )
            result = riveter.runs.wait_for_result(run.id)
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            run = riveter.configured_datasets.build(
              "cds_YOUR_CONFIGURED_DATASET_ID",
              parameters: { "City" => "Denver" }
            )
            result = riveter.runs.wait_for_result(run.id)
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            ctx := context.Background()
            run, err := client.ConfiguredDatasets.Build(ctx, "cds_YOUR_CONFIGURED_DATASET_ID",
                riveter.BuildConfiguredDatasetParams{Parameters: map[string]string{"City": "Denver"}})
            result, err := client.Runs.WaitForResult(ctx, run.ID, nil)
      tags:
        - List builds
      parameters:
        - name: id
          in: path
          required: true
          description: Configured dataset id (cds_...)
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                parameters:
                  type: object
                  description: Values for the template's placeholders (keys defined by the template)
                tier:
                  type: string
                  description: Optional pricing/depth tier when the template defines tiers
                dataset_webhook_url:
                  type: string
                  format: uri
                  description: URL to POST the dataset results to when the build completes
                estimate_cost:
                  $ref: "#/components/schemas/EstimateCostParam"
                max_credits:
                  $ref: "#/components/schemas/MaxCreditsParam"
            examples:
              with_parameters:
                summary: Fill in template parameters
                value:
                  parameters:
                    practice_type: "dentists"
                    state: "ohio"
      responses:
        "201":
          description: Build started — the run plus dataset_id / configured_dataset_id / max_items / tier / credits_charged
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Run"
                  - type: object
                    properties:
                      dataset_id:
                        type: string
                      configured_dataset_id:
                        type: string
                      max_items:
                        type: integer
                      tier:
                        type: [string, "null"]
                      credits_charged:
                        type: number
        "200":
          description: estimate_cost only — the tier's flat price; nothing was started
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EstimateCostResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /extractions/{id}/runs:
    post:
      summary: run extraction
      description: |
        Execute a `ready` extraction. Returns a run — poll [GET /runs/{id}](#tag/runs/get/runs/{id}) and fetch the extracted records with [GET /runs/{id}/result](#tag/runs/get/runs/{id}/result) (the records come back as an array of JSON objects matching your schema), or pass a `webhook_url`.

        `variables` fills any `{{ placeholder }}` values the plan defines (e.g. a search term or location). Each run charges run credits (`credits_charged`).
      operationId: runExtraction
      x-mcp-open-world: true
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const run = await riveter.extractions.run("ext_YOUR_EXTRACTION_ID", {
              variables: { location: "Denver" },
            });
            const result = await riveter.runs.waitForResult(run.id);
            console.log(result.output);
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            run = riveter.extractions.run(
                "ext_YOUR_EXTRACTION_ID",
                variables={"location": "Denver"},
            )
            result = riveter.runs.wait_for_result(run.id)
            print(result.output)
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            run = riveter.extractions.run(
              "ext_YOUR_EXTRACTION_ID",
              variables: { "location" => "Denver" }
            )
            result = riveter.runs.wait_for_result(run.id)
            puts result.output
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            ctx := context.Background()
            run, err := client.Extractions.Run(ctx, "ext_YOUR_EXTRACTION_ID",
                riveter.RunExtractionParams{Variables: map[string]string{"location": "Denver"}})
            result, err := client.Runs.WaitForResult(ctx, run.ID, nil)
            fmt.Println(string(result.Output))
      tags:
        - Extractions
      parameters:
        - name: id
          in: path
          required: true
          description: The extraction id (ext_...)
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                variables:
                  type: object
                  description: Values for the plan's placeholders
                webhook_url:
                  type: string
                  format: uri
                  description: URL to POST the records to when the run completes
                estimate_cost:
                  $ref: "#/components/schemas/EstimateCostParam"
                max_credits:
                  $ref: "#/components/schemas/MaxCreditsParam"
      responses:
        "201":
          description: Extraction run started — the run plus merged variables / credits_charged (and a validation_warning when the plan's last discovery validation did not pass)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Run"
                  - type: object
                    properties:
                      variables:
                        type: object
                      credits_charged:
                        type: number
                      validation_warning:
                        type: string
                        description: Present when the last discovery validation did not pass
        "200":
          description: estimate_cost only — the flat per-run price (also shown as run_credits_required on GET /extractions/{id}); nothing was started
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EstimateCostResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /extractions:
    post:
      summary: create extraction
      description: |
        Create an **extraction** — a reusable recipe for scraping structured records from a website — and start its agent discovery. Discovery explores the site, builds the scrape/extract plan, and validates it against your schema.

        Poll [GET /extractions/{id}](#tag/extractions/get/extractions/{id}) until `status` is `ready`, then execute it with [POST /extractions/{id}/runs](#tag/extractions/post/extractions/{id}/runs). The extraction (`ext_...`) and its runs (`run_...`) are different resources.

        Creating an extraction charges discovery credits (returned as `credits_charged`). To see the price first, send `estimate_cost: true`.
      operationId: createExtraction
      x-mcp-open-world: true
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const extraction = await riveter.extractions.create({
              starting_url: "https://example.com/directory",
              goal_description: "Extract every listed company",
              output_record_json_schema: {
                type: "object",
                properties: { name: { type: "string" }, website: { type: "string" } },
              },
            });
            console.log(extraction.id); // ext_...
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            extraction = riveter.extractions.create(
                starting_url="https://example.com/directory",
                goal_description="Extract every listed company",
                output_record_json_schema={
                    "type": "object",
                    "properties": {"name": {"type": "string"}, "website": {"type": "string"}},
                },
            )
            print(extraction.id)  # ext_...
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            extraction = riveter.extractions.create(
              starting_url: "https://example.com/directory",
              goal_description: "Extract every listed company",
              output_record_json_schema: {
                "type" => "object",
                "properties" => { "name" => { "type" => "string" }, "website" => { "type" => "string" } }
              }
            )
            puts extraction.id # ext_...
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            extraction, err := client.Extractions.Create(context.Background(),
                riveter.CreateExtractionParams{
                    StartingURL:     "https://example.com/directory",
                    GoalDescription: "Extract every listed company",
                    OutputRecordJSONSchema: map[string]any{
                        "type": "object",
                        "properties": map[string]any{
                            "name":    map[string]any{"type": "string"},
                            "website": map[string]any{"type": "string"},
                        },
                    },
                })
            fmt.Println(extraction.ID) // ext_...
      tags:
        - Extractions
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                starting_url:
                  type: string
                  format: uri
                  description: Where the agent starts exploring
                goal_description:
                  type: string
                  description: What records to extract, in plain language
                output_record_json_schema:
                  type: object
                  description: JSON schema of one output record
                name:
                  type: string
                  description: Optional display name
                required_keys:
                  type: array
                  items:
                    type: string
                  description: Record keys that must be non-empty for a record to count
                estimate_cost:
                  $ref: "#/components/schemas/EstimateCostParam"
                max_credits:
                  $ref: "#/components/schemas/MaxCreditsParam"
              required:
                - starting_url
                - goal_description
                - output_record_json_schema
            examples:
              create:
                summary: Create an extraction
                value:
                  starting_url: "https://example.com/products"
                  goal_description: "Extract every product with its name and price"
                  output_record_json_schema:
                    type: object
                    properties:
                      name:
                        type: string
                      price:
                        type: string
                  required_keys: ["name"]
      responses:
        "201":
          description: Extraction created, discovery started
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    description: Extraction id (ext_...)
                  name:
                    type: string
                  status:
                    type: string
                    enum: [discovering]
                  app_url:
                    type: string
                    format: uri
                  credits_charged:
                    type: number
                  credit_detail:
                    $ref: "#/components/schemas/CreditDetail"
                  required_keys:
                    type: array
                    items:
                      type: string
        "200":
          description: estimate_cost only — the discovery price (type extraction_discovery); nothing was created or charged
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EstimateCostResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /extractions/{id}:
    get:
      summary: get extraction
      description: |
        The extraction's status and definition. `status` is `discovering` while the agent builds the plan, then `ready` (or `discovery_failed`). Once `ready`, execute with [POST /extractions/{id}/runs](#tag/extractions/post/extractions/{id}/runs).
      operationId: getExtraction
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const extraction = await riveter.extractions.get("ext_YOUR_EXTRACTION_ID");
            console.log(extraction.status); // "ready" once discovery finishes
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            extraction = riveter.extractions.get("ext_YOUR_EXTRACTION_ID")
            print(extraction.status)  # "ready" once discovery finishes
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            extraction = riveter.extractions.get("ext_YOUR_EXTRACTION_ID")
            puts extraction.status # "ready" once discovery finishes
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            extraction, err := client.Extractions.Get(context.Background(), "ext_YOUR_EXTRACTION_ID")
            fmt.Println(extraction.Status) // "ready" once discovery finishes
      tags:
        - Extractions
      parameters:
        - name: id
          in: path
          required: true
          description: The extraction id (ext_...)
          schema:
            type: string
      responses:
        "200":
          description: Extraction status and definition
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Extraction"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    # (No PATCH/DELETE — manage extractions from the app UI.)

  /monitors:
    post:
      summary: create monitor
      description: |
        Create a **monitor**: a schedule that re-runs an enrichment daily, weekly, or monthly over fixed `input` rows and can POST results (or only changes) to a webhook.

        Creating the monitor charges nothing. Every scheduled run charges the enrichment price for the `input` rows. `estimate_cost: true` returns the credit estimate for **one** scheduled run; `max_credits` refuses the monitor when that estimate is higher. Without `input`, scheduled runs have no rows and fail with `no_monitor_input`.
      x-mcp-description: |
        Create a **monitor**: a schedule that re-runs an enrichment daily, weekly, or monthly over fixed `input` rows and can POST results (or only changes) to a webhook. Creation is free; every scheduled run charges the enrichment price for `input`. `estimate_cost: true` prices one run. Without `input`, runs fail with `no_monitor_input`.
      operationId: createMonitor
      x-mcp-open-world: true
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const monitor = await riveter.monitors.create({
              enrichment_id: "enr_YOUR_ENRICHMENT_ID",
              cadence: "daily",
              minute: 0,
              hour: 9,
              timezone: "America/New_York",
              webhook_url: "https://your-server.com/webhook",
            });
            console.log(monitor.id, monitor.next_run_at);
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            monitor = riveter.monitors.create(
                enrichment_id="enr_YOUR_ENRICHMENT_ID",
                cadence="daily",
                minute=0,
                hour=9,
                timezone="America/New_York",
                webhook_url="https://your-server.com/webhook",
            )
            print(monitor.id, monitor.next_run_at)
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            monitor = riveter.monitors.create(
              enrichment_id: "enr_YOUR_ENRICHMENT_ID",
              cadence: "daily",
              minute: 0,
              hour: 9,
              timezone: "America/New_York",
              webhook_url: "https://your-server.com/webhook"
            )
            puts "#{monitor.id} #{monitor.next_run_at}"
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            monitor, err := client.Monitors.Create(context.Background(),
                riveter.CreateMonitorParams{
                    EnrichmentID: "enr_YOUR_ENRICHMENT_ID",
                    Cadence:      "daily",
                    Minute:       0,
                    Hour:         9,
                    Timezone:     "America/New_York",
                    WebhookURL:   "https://your-server.com/webhook",
                })
            fmt.Println(monitor.ID, monitor.NextRunAt)
      tags:
        - Monitors
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                enrichment_id:
                  type: string
                  description: Id of the enrichment to monitor (enr_...)
                cadence:
                  type: string
                  enum: [daily, weekly, monthly]
                  description: How often the monitor runs
                minute:
                  type: integer
                  minimum: 0
                  maximum: 59
                  description: Minute of the hour to run
                hour:
                  type: integer
                  minimum: 0
                  maximum: 23
                  description: Hour of the day to run
                day_of_week:
                  type: integer
                  minimum: 0
                  maximum: 6
                  description: Day of the week (0=Sunday, required for weekly)
                day_of_month:
                  type: integer
                  minimum: 1
                  maximum: 28
                  description: Day of the month (required for monthly)
                timezone:
                  type: string
                  description: "Timezone (e.g. 'UTC', 'America/New_York')"
                webhook_url:
                  type: string
                  format: uri
                  description: URL to receive results each scheduled run
                alert_rule:
                  type: string
                  enum: [each_run, only_on_change]
                  description: When to send alerts (default each_run)
                output_format:
                  type: string
                  enum: [current_only, current_and_previous]
                  description: Output format (default current_only)
                run_immediately:
                  type: boolean
                  description: Also run the monitor immediately after creation (a paid run)
                input:
                  $ref: "#/components/schemas/EnrichmentInputData"
                  description: Fixed rows every scheduled run enriches (same shape as enrich input); runs fail without it
                estimate_cost:
                  $ref: "#/components/schemas/EstimateCostParam"
                max_credits:
                  $ref: "#/components/schemas/MaxCreditsParam"
              required:
                - enrichment_id
                - cadence
                - minute
                - hour
                - timezone
            examples:
              daily_monitor:
                summary: Daily monitor with change alerts
                value:
                  enrichment_id: "enr_018f5b60-1234-7abc-89ab-0123456789ab"
                  cadence: daily
                  hour: 9
                  minute: 0
                  timezone: "UTC"
                  alert_rule: only_on_change
                  webhook_url: "https://your-server.com/webhook"
                  input:
                    "Company Name": ["Acme Corp", "Tech Solutions Inc"]
      responses:
        "201":
          description: Monitor created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Monitor"
        "200":
          description: estimate_cost only — the credit estimate for ONE scheduled run over `input` (type monitor); nothing was created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EstimateCostResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
    get:
      summary: list monitors
      description: List the account's monitors, newest first.
      operationId: listMonitors
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const monitors = await riveter.monitors.list();
            for (const monitor of monitors) {
              console.log(monitor.id, monitor.schedule_summary);
            }
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            for monitor in riveter.monitors.list():
                print(monitor.id, monitor.schedule_summary)
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            riveter.monitors.list.each do |monitor|
              puts "#{monitor.id} #{monitor.schedule_summary}"
            end
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            monitors, err := client.Monitors.List(context.Background())
            for _, monitor := range monitors {
                fmt.Println(monitor.ID, monitor.ScheduleSummary)
            }
      tags:
        - Monitors
      responses:
        "200":
          description: Monitors listed
          content:
            application/json:
              schema:
                type: object
                properties:
                  monitors:
                    type: array
                    items:
                      $ref: "#/components/schemas/Monitor"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /monitors/{id}:
    get:
      summary: get monitor
      description: The monitor's schedule, webhook, and next run time.
      operationId: getMonitor
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const monitor = await riveter.monitors.get("mon_YOUR_MONITOR_ID");
            console.log(monitor.enabled, monitor.next_run_at);
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            monitor = riveter.monitors.get("mon_YOUR_MONITOR_ID")
            print(monitor.enabled, monitor.next_run_at)
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            monitor = riveter.monitors.get("mon_YOUR_MONITOR_ID")
            puts "#{monitor.enabled} #{monitor.next_run_at}"
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            monitor, err := client.Monitors.Get(context.Background(), "mon_YOUR_MONITOR_ID")
            fmt.Println(monitor.Enabled, monitor.NextRunAt)
      tags:
        - Monitors
      parameters:
        - name: id
          in: path
          required: true
          description: The monitor id (mon_...)
          schema:
            type: string
      responses:
        "200":
          description: The monitor
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Monitor"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      summary: update monitor
      description: |
        Pause, resume, or repoint a monitor. `enabled: false` pauses, `enabled: true` resumes; `webhook_url` replaces the delivery URL.
      operationId: updateMonitor
      x-mcp-open-world: true
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            // Pause the monitor:
            const monitor = await riveter.monitors.update("mon_YOUR_MONITOR_ID", {
              enabled: false,
            });
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            # Pause the monitor:
            monitor = riveter.monitors.update("mon_YOUR_MONITOR_ID", enabled=False)
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            # Pause the monitor:
            monitor = riveter.monitors.update("mon_YOUR_MONITOR_ID", enabled: false)
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            // Pause the monitor (Enabled is a pointer so `false` still serializes):
            enabled := false
            monitor, err := client.Monitors.Update(context.Background(), "mon_YOUR_MONITOR_ID",
                riveter.UpdateMonitorParams{Enabled: &enabled})
      tags:
        - Monitors
      parameters:
        - name: id
          in: path
          required: true
          description: The monitor id (mon_...)
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                enabled:
                  type: boolean
                  description: false pauses the monitor, true resumes it
                webhook_url:
                  type: string
                  format: uri
            examples:
              pause:
                summary: Pause
                value:
                  enabled: false
              resume:
                summary: Resume
                value:
                  enabled: true
      responses:
        "200":
          description: The updated monitor
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Monitor"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /monitors/{id}/runs:
    get:
      summary: list monitor runs
      description: |
        The monitor's run history, newest first. Fetch a specific run's data with [GET /runs/{id}/result](#tag/runs/get/runs/{id}/result). Supports `status`, `page`, and `per_page`.
      operationId: listMonitorRuns
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const page = await riveter.monitors.runs("mon_YOUR_MONITOR_ID");
            for await (const run of page) { // auto-pages through every result
              console.log(run.id, run.status);
            }
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            page = riveter.monitors.runs("mon_YOUR_MONITOR_ID")
            for run in page.auto_paging_iter():  # pages through every result
                print(run.id, run.status)
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            page = riveter.monitors.runs("mon_YOUR_MONITOR_ID")
            page.auto_paging_each do |run| # pages through every result
              puts "#{run.id} #{run.status}"
            end
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            ctx := context.Background()
            page, err := client.Monitors.Runs(ctx, "mon_YOUR_MONITOR_ID", nil)
            for {
                for _, run := range page.Runs {
                    fmt.Println(run.ID, run.Status)
                }
                if !page.HasNextPage() {
                    break
                }
                page, err = page.NextPage(ctx)
            }
      tags:
        - Monitors
      parameters:
        - name: id
          in: path
          required: true
          description: The monitor id (mon_...)
          schema:
            type: string
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [pending, enqueued, processing, success, stopped]
        - name: page
          in: query
          required: false
          schema:
            type: integer
            default: 1
        - name: per_page
          in: query
          required: false
          schema:
            type: integer
            default: 25
            maximum: 100
      responses:
        "200":
          description: Monitor runs listed
          content:
            application/json:
              schema:
                type: object
                properties:
                  monitor_id:
                    type: string
                  runs:
                    type: array
                    items:
                      $ref: "#/components/schemas/RunListItem"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /quick_search:
    post:
      summary: quick search
      description: |
        Run a web search and get structured results back **synchronously** — the response is the run with the results already in `output` (no polling, no webhook). Optionally filter to a date range with `date_start` / `date_end` (format `YYYY-MM-DD`); if only `date_start` is given, `date_end` defaults to today.

        The result is also stored on the run, so it stays re-fetchable at [GET /runs/{id}/result](#tag/runs/get/runs/{id}/result).

        For bulk searches — up to **100,000** per request, async with `webhook_url` support — use the legacy [POST /v1/web_search](./openapi.legacy.yaml) endpoint.

        ## Quick example
        ```bash
        curl -X POST https://api.riveterhq.com/v1/quick_search \
          -H "Authorization: Bearer YOUR_API_KEY" \
          -H "Content-Type: application/json" \
          -d '{"query": "latest OpenAI news"}'
        ```

        ## Credit costs
        - **0.04 credits** per search.
      operationId: quickSearch
      x-mcp-open-world: true
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const run = await riveter.quickSearch({ query: "Riveter data enrichment" });
            console.log(run.output); // synchronous — results are already here
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            run = riveter.quick_search("Riveter data enrichment")
            print(run.output)  # synchronous — results are already here
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            run = riveter.quick_search(query: "Riveter data enrichment")
            puts run.output # synchronous — results are already here
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            run, err := client.QuickSearch(context.Background(),
                riveter.QuickSearchParams{Query: "Riveter data enrichment"})
            fmt.Println(string(run.Output)) // synchronous — results are already here
      tags:
        - Tools
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                query:
                  type: string
                  description: The search query (simpler is better)
                date_start:
                  type: string
                  description: "Optional start date filter, format YYYY-MM-DD"
                date_end:
                  type: string
                  description: "Optional end date filter, format YYYY-MM-DD. Defaults to today if date_start is set"
                run_key:
                  type: string
                  maxLength: 255
                  pattern: "^[A-Za-z0-9._~-]+$"
                  description: Optional idempotency key (becomes the run id "run_<run_key>")
                estimate_cost:
                  $ref: "#/components/schemas/EstimateCostParam"
                max_credits:
                  $ref: "#/components/schemas/MaxCreditsParam"
              required:
                - query
            examples:
              simple_search:
                summary: A simple search
                value:
                  query: "latest OpenAI news"
              date_filtered_search:
                summary: A search filtered to a date range
                value:
                  query: "OpenAI GPT-4o mini"
                  date_start: "2024-07-01"
                  date_end: "2024-07-31"
      responses:
        "200":
          description: Search completed — the run with the results in `output` (or, with estimate_cost, the credit estimate only)
          content:
            application/json:
              schema:
                oneOf:
                  - allOf:
                      - $ref: "#/components/schemas/Run"
                      - type: object
                        properties:
                          output:
                            description: 'The search results: `{ "results": [{ "title", "link", "snippet" }, ...], "knowledge_graph"? }`'
                            type: object
                  - $ref: "#/components/schemas/EstimateCostResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /search_agent:
    post:
      summary: search agent
      description: |
        Ask a question and get an answer. This is meant for quick, relatively scoped one-off questions, like "What is the NAICS code for this company...". For more complex questions, use the `enrich/` endpoint.

        This uses the same AI + web-tool loop that fills a single agent-mode cell in an enrichment (web search, web scraping, PDF reading, HTTP requests), with no enrichment setup.

        The run is processed in the background while this request **long-polls up to `wait` seconds (default 50)** — most runs finish in time and return the answer inline in `output.result`. If the agent is still working when the budget elapses, the response comes back with `status: processing` and `output: null`; poll [GET /runs/{id}/result](#tag/runs/get/runs/{id}/result) (it long-polls too) until the run is terminal.

        Pass `output_schema` (a JSON Schema object) to get `output.result` back as a structured object matching your schema instead of free text. Unanswerable questions return the string `"not found"`.

        ## Quick example
        ```bash
        curl -X POST https://api.riveterhq.com/v1/search_agent \
          -H "Authorization: Bearer YOUR_API_KEY" \
          -H "Content-Type: application/json" \
          -d '{"prompt": "Who is the current CEO of Anthropic, and when did they take the role?"}'
        ```

        ## Credit costs
        - **1 credit** per call (same as one agent-mode enrichment cell), charged when the agent completes. Failed runs are not charged.
      operationId: searchAgent
      x-mcp-open-world: true
      tags:
        - Tools
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                prompt:
                  type: string
                  maxLength: 50000
                  description: The question or task for the agent
                output_schema:
                  type: object
                  description: 'Optional JSON Schema object for the answer, e.g. `{"type": "object", "properties": {"ceo_name": {"type": "string"}}}`. When set, `output.result` is an object matching this schema.'
                wait:
                  type: integer
                  minimum: 0
                  maximum: 50
                  description: Seconds to hold this request waiting for the answer (default and max 50). Pass 0 to return immediately and poll GET /runs/{id}/result instead.
                run_key:
                  type: string
                  maxLength: 255
                  pattern: "^[A-Za-z0-9._~-]+$"
                  description: Optional idempotency key (becomes the run id "run_<run_key>")
                estimate_cost:
                  $ref: "#/components/schemas/EstimateCostParam"
                max_credits:
                  $ref: "#/components/schemas/MaxCreditsParam"
              required:
                - prompt
            examples:
              free_text_question:
                summary: A free-text question
                value:
                  prompt: "Who is the current CEO of Anthropic, and when did they take the role?"
              structured_answer:
                summary: A structured answer via output_schema
                value:
                  prompt: "Find the founding year and headquarters city of Anthropic"
                  output_schema:
                    type: object
                    properties:
                      founding_year:
                        type: integer
                      headquarters_city:
                        type: string
      responses:
        "201":
          description: "The run — with the answer in `output.result` when it finished within `wait`, or `status: processing` and `output: null` when the agent is still working"
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Run"
                  - type: object
                    properties:
                      output:
                        type: [object, "null"]
                        description: '`{ "result": <string or object> }` once the run finishes; null while it is still processing'
        "200":
          description: estimate_cost only — the flat per-call price; nothing was started
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EstimateCostResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /scrape:
    post:
      summary: scrape
      description: |
        Scrape a webpage and return the text content **synchronously** — the only endpoint here that doesn't return a run to poll. Unchanged from the legacy API (response uses the legacy `request_status` format).

        ## Quick example
        ```bash
          curl -X POST https://api.riveterhq.com/v1/scrape \
          -H "Authorization: Bearer YOUR_API_KEY" \
          -H "Content-Type: application/json" \
          -d '{"url": "https://example.com"}'
        ```

        ## Credit costs
        - **With proxy**: 0.30 credits
        - **Without proxy**: 0.10 credits
        - **From cache**: 0.01 credits

        ## Proxy usage
        Scraping is not guaranteed to succeed without a proxy. Some websites may block requests or require specific geographic locations. To use a proxy, include `proxy_country_code` with a two-character country code (e.g. 'us', 'gb', 'de').

        ## Caching
        Recently scraped pages are cached to save credits (0.01 credits on a cache hit). Set `skip_cache: true` to always fetch fresh content.
      operationId: scrape
      x-mcp-open-world: true
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const page = await riveter.scrape({ url: "https://example.com" });
            console.log(page.text); // synchronous — no run to poll
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            page = riveter.scrape("https://example.com")
            print(page.text)  # synchronous — no run to poll
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            page = riveter.scrape(url: "https://example.com")
            puts page.text # synchronous — no run to poll
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            page, err := client.Scrape(context.Background(),
                riveter.ScrapeParams{URL: "https://example.com"})
            fmt.Println(page.Text) // synchronous — no run to poll
      tags:
        - Tools
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
                  description: The URL to scrape
                proxy_country_code:
                  type: string
                  description: Optional two-character country code for proxy (e.g. 'us', 'gb', 'de')
                  pattern: "^[a-z]{2}$"
                skip_cache:
                  type: boolean
                  description: Set to true to bypass cache and always fetch fresh content
                  default: false
                estimate_cost:
                  $ref: "#/components/schemas/EstimateCostParam"
                max_credits:
                  $ref: "#/components/schemas/MaxCreditsParam"
              required:
                - url
      responses:
        "200":
          description: Webpage scraped successfully (or, with estimate_cost, the credit estimate only — minimum is the cache-hit price, maximum depends on proxy_country_code)
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ScrapeResponse"
                  - $ref: "#/components/schemas/EstimateCostResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /tools:
    get:
      summary: list agent tools
      description: |
        The tools an enrichment may name in `only_tools` / `additional_tools`: what each does, its credits per call, whether it is on by default, and whether it is enabled on this account. Some tools call their provider with your own API key (`requires_api_key`, linked under Settings > API keys). `budget` is the per-row credit range and the per-column default.
      operationId: listTools
      x-badges:
        - name: Beta
          color: "#e9d5ff"
      x-mcp-description: |
        Datasets beta, per account (else 403 `feature_not_enabled`). The tools an enrichment may list in `only_tools` / `additional_tools`: name, what it does, credits per call (`price_note` when the price is not flat — the LinkedIn email / phone tools are charged only when they return a contact, `text_search_pdf` per page), whether it is on by default, whether it needs opt-in and is enabled on this account, and for a tool that uses the account's own provider key (Apollo, Hunter, Findymail, curl, USPTO) whether one is linked (`api_key_linked`; refused until it is). Also the `budget` (credits per row) range and per-column default.
      tags:
        - Datasets (beta)
      responses:
        "200":
          description: The tool catalog for this account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ToolCatalog"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /account:
    get:
      summary: account
      description: |
        Information about the account associated with the API key: plan, credit balance, and the key's metadata. Unchanged from the legacy API (response uses the legacy `request_status` format).
      operationId: getAccount
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: |
            import { Riveter } from "riveter-sdk";

            const riveter = new Riveter(); // uses env RIVETER_API_KEY
            const info = await riveter.account();
            console.log(info.account.credit.balance);
        - lang: python
          label: Python
          source: |
            from riveter import Riveter

            riveter = Riveter()  # uses env RIVETER_API_KEY
            info = riveter.account()
            print(info.account.credit.balance)
        - lang: ruby
          label: Ruby
          source: |
            require "riveter"

            riveter = Riveter::Client.new # uses env RIVETER_API_KEY
            info = riveter.account
            puts info.account.credit.balance
        - lang: go
          label: Go
          source: |
            import riveter "github.com/riveterhq/riveter-go"

            client, err := riveter.NewClient() // uses env RIVETER_API_KEY
            info, err := client.Account(context.Background())
            fmt.Println(info.Account.Credit.Balance)
      tags:
        - Account
      responses:
        "200":
          description: Account information retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  request_status:
                    type: string
                    enum: [success]
                  message:
                    type: string
                  account:
                    $ref: "#/components/schemas/Account"
                  api_key_info:
                    $ref: "#/components/schemas/ApiKeyInfo"
                required:
                  - request_status
                  - message
                  - account
                  - api_key_info
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: API_KEY
      description: API key authentication. Use 'Bearer YOUR_API_KEY' in the Authorization header.
      x-scalar-secret-token: YOUR_API_KEY

  parameters:
    DatasetId:
      name: id
      in: path
      required: true
      description: The dataset id (ds_...)
      schema:
        type: string

  schemas:
    Run:
      type: object
      description: |
        The uniform shape for every run, returned by kickoff endpoints and all /runs endpoints.
        Related resource ids (enrichment_id, dataset_id, extraction_id, monitor_id) appear when they
        apply to the run; kickoff responses may add a few endpoint-specific fields.
      properties:
        id:
          type: string
          description: The run id (run_...)
        type:
          type: string
          enum:
            [
              enrichment,
              dataset_build,
              extraction,
              scrape,
              quick_search,
              search_agent,
            ]
          description: What kind of run this is
        status:
          type: string
          enum: [pending, enqueued, processing, success, stopped]
        progress:
          $ref: "#/components/schemas/RunProgress"
        credits_used:
          type: number
          description: Credits consumed so far
        credit_detail:
          $ref: "#/components/schemas/CreditDetail"
        app_url:
          type: string
          format: uri
          description: Link to view this run in the Riveter app
        result_url:
          type: string
          format: uri
          description: Where to fetch the run's output
        started_at:
          type: [string, "null"]
          format: date-time
        finished_at:
          type: [string, "null"]
          format: date-time
        error:
          type: [object, "null"]
          description: Null unless the run hit an error
          properties:
            type:
              type: string
            message:
              type: string
        stopped_reason:
          $ref: "#/components/schemas/StoppedReason"
        enrichment_id:
          type: string
          description: Present when the run belongs to an enrichment
        enrichment_name:
          type: string
        dataset_id:
          type: string
          description: Present on dataset-build runs
        extraction_id:
          type: string
          description: Present on extraction runs
        monitor_id:
          type: string
          description: Present on monitor-scheduled runs
        webhook_url:
          type: string
          format: uri
          description: Present when the run has a webhook configured
      required:
        - id
        - type
        - status
        - progress
        - credit_detail
        - result_url
      example:
        id: "run_018f6a70-1234-7abc-89ab-0123456789ab"
        type: "enrichment"
        status: "processing"
        progress:
          percent_complete: 40
          estimated_seconds_remaining: 90
          elapsed_seconds: 60
          completed_cells: 4
          total_cells_expected: 10
          not_found_cells: 0
        credits_used: 2.5
        credit_detail:
          estimate:
            minimum: 0
            maximum: 10
            charged_upfront: false
          credits_used: 2.5
        app_url: "https://app.riveterhq.com/runs/run_018f6a70-1234-7abc-89ab-0123456789ab"
        result_url: "https://api.riveterhq.com/v1/runs/run_018f6a70-1234-7abc-89ab-0123456789ab/result"
        started_at: "2026-01-15T12:00:00Z"
        finished_at: null
        error: null
        enrichment_id: "enr_018f5b60-1234-7abc-89ab-0123456789ab"
        enrichment_name: "My Enrichment"

    EstimateCostParam:
      type: boolean
      default: false
      description: |
        Validate and price the request without running or charging anything; the 200 response is the
        credit estimate (EstimateCostResult).

    MaxCreditsParam:
      type: number
      minimum: 0
      description: |
        Credit ceiling. Refused with 422 credit_cap_exceeded (nothing charged) when the estimate's
        maximum exceeds it.

    CreditEstimate:
      type: object
      description: |
        Kickoff-time credit estimate. `maximum` is the contract — the run never charges more.
        `minimum` is 0 when the run can charge less (cells that short-circuit, unfound dataset rows refunded).
        `expected` is the likely charge when history exists (a dataset fill: rows × the enrichment's average charge per row over its finished fills).
      properties:
        minimum:
          type: number
          description: Least the run can cost
        expected:
          type: number
          description: Likely cost from past runs of the same config; absent without history
        maximum:
          type: number
          description: Most the run can cost
        charged_upfront:
          type: boolean
          description: true when `maximum` is taken at kickoff (dataset builds, extractions); false when credits accrue as work completes
      required:
        - minimum
        - maximum
        - charged_upfront

    CreditDetail:
      type: object
      description: |
        The estimate made at kickoff next to what has been charged so far. `estimate` is null for runs
        started before estimates existed. Estimates and credit errors add `credits_remaining` and `sufficient_credits`.
      properties:
        estimate:
          oneOf:
            - $ref: "#/components/schemas/CreditEstimate"
            - type: "null"
        credits_used:
          type: number
          description: Credits charged so far (same value as the top-level credits_used)
        credits_remaining:
          type: number
          description: Account balance. Only on estimates and credit errors.
        sufficient_credits:
          type: boolean
          description: Whether the balance covers `estimate.maximum`. Only on estimates and credit errors.
      required:
        - estimate
        - credits_used

    EstimateCostResult:
      type: object
      description: |
        Returned with `200` when a paid endpoint is called with `estimate_cost: true`. The request was validated
        and priced; nothing was created or charged.
      properties:
        estimate_cost:
          type: boolean
          enum: [true]
        type:
          type: string
          enum:
            [
              enrichment,
              dataset_build,
              extraction_discovery,
              extraction,
              monitor,
              scrape,
              quick_search,
              search_agent,
            ]
          description: What would have been charged for (`monitor` = one scheduled run of the monitor)
        credit_detail:
          $ref: "#/components/schemas/CreditDetail"
      required:
        - estimate_cost
        - type
        - credit_detail
      example:
        estimate_cost: true
        type: "enrichment"
        credit_detail:
          estimate:
            minimum: 0
            maximum: 1920
            charged_upfront: false
          credits_used: 0
          credits_remaining: 5000
          sufficient_credits: true

    RunProgress:
      type: object
      description: Completion estimate. Cell-based runs (enrichment, quick_search) also report cell counts; dataset fills report row counts; builds on a dataset report what landed.
      properties:
        percent_complete:
          type: [number, "null"]
        estimated_seconds_remaining:
          type: [number, "null"]
        elapsed_seconds:
          type: [number, "null"]
        completed_cells:
          type: integer
          description: Cell-based runs only
        total_cells_expected:
          type: integer
          description: Cell-based runs only
        not_found_cells:
          type: [integer, "null"]
          description: Cell-based runs only. Null while a run over 5,000 rows is in progress; set when it finishes.
        total_rows:
          type: integer
          description: Dataset fills only. Rows in the fill's scope
        completed_rows:
          type: integer
          description: Dataset fills only. Rows finished (ok, not found, or failed)
        running_rows:
          type: integer
          description: Dataset fills only. Rows an agent is working on right now
        requested_rows:
          type: integer
          description: Builds on a dataset only. The `count` asked for
        generated_rows:
          type: integer
          description: Builds on a dataset only. Rows the generator found; set when the run finishes
        added_rows:
          type: integer
          description: Builds on a dataset only. Rows that landed in the dataset; set when the run finishes
        skipped_duplicate_rows:
          type: integer
          description: Builds on a dataset only. Generated rows dropped as duplicates on the index column; set when the run finishes

    RunListItem:
      type: object
      description: The slim run shape used by GET /runs and GET /monitors/{id}/runs.
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            [
              enrichment,
              dataset_build,
              extraction,
              scrape,
              quick_search,
              search_agent,
            ]
        status:
          type: string
          enum: [pending, enqueued, processing, success, stopped]
        enrichment_id:
          type: [string, "null"]
        enrichment_name:
          type: [string, "null"]
        row_count:
          type: [integer, "null"]
        credits_used:
          type: number
        error:
          type: [object, "null"]
          properties:
            type:
              type: string
            message:
              type: string
        stopped_reason:
          $ref: "#/components/schemas/StoppedReason"
        created_at:
          type: string
          format: date-time
        started_at:
          type: [string, "null"]
          format: date-time
        finished_at:
          type: [string, "null"]
          format: date-time
        app_url:
          type: string
          format: uri
        result_url:
          type: string
          format: uri

    StoppedReason:
      type: string
      enum: [user_request, credit_limit, error]
      description: |
        Why the run has status `stopped`. Only present on stopped runs.
        `user_request`: stopped through the API or the app. `credit_limit`: the account ran out of credits.
        `error`: the run failed (internal error, or every enrichment cell failed); `error.message` has the detail.

    Pagination:
      type: object
      properties:
        page:
          type: integer
        per_page:
          type: integer
        total_count:
          type: integer
        total_pages:
          type: integer

    OutputFile:
      type: object
      description: One export of a run's whole grid on object storage. `url` is a signed link, valid until `expires_at`; re-fetch the result for a fresh one.
      properties:
        state:
          type: string
          enum: [pending, ready]
        format:
          type: string
          enum: [csv, jsonl]
        url:
          type: string
          format: uri
        expires_at:
          type: string
          format: date-time
        byte_size:
          type: integer
        row_count:
          type: integer
      required:
        - state
        - format

    OutputFiles:
      type: object
      description: The run's output as files, on every dataset fill (`POST /enrich` with `dataset_id` or `input`, `POST /enrich_rows`). Both are written together once the run finishes.
      properties:
        csv:
          $ref: "#/components/schemas/OutputFile"
        jsonl:
          $ref: "#/components/schemas/OutputFile"

    EnrichmentInputData:
      type: object
      description: |
        Keys are your source-data column headers. Values are arrays of strings (one per row).
        Any column header name is allowed; all input columns must have the same array length.
        Row caps: 10,000 rows with enrichment_id, 1,000 rows with an inline config.
      additionalProperties:
        type: array
        description: One string per row (all input columns must have the same array length)
        items:
          type: string
        maxItems: 10000
      example:
        "Company Name": ["Acme Corp", "Tech Solutions Inc"]
        "Website": ["acme.com", "techsolutions.com"]

    FormatDetails:
      type: object
      description: |
        Format-specific options. Valid keys depend on the column `format`.
        Only include keys that apply to your chosen format.
      properties:
        options:
          type: array
          items:
            type: string
          description: tag — allowed tag values (required for tag format)
        allow_multiple:
          type: boolean
          description: tag — allow selecting more than one tag
        descriptions:
          type: object
          description: tag — optional map from tag value to description (keys must be in `options`)
          additionalProperties:
            type: string
        decimal_places:
          type: integer
          minimum: 0
          maximum: 9
          description: number — decimal places to round to
        currency_code:
          type: string
          description: number — 3-letter currency code (mutually exclusive with percentage)
        commas:
          type: boolean
          description: number — display thousands separators
        percentage:
          type: boolean
          description: number — format as percentage (mutually exclusive with currency_code)
        description:
          type: string
          description: json — natural-language schema description (use with or instead of `schema`)
        schema:
          type: object
          description: |
            json — JSON Schema object for structured output. Strict-mode requirements
            (`additionalProperties: false`, full `required` arrays) are applied automatically.
            Mark optional fields nullable (`"type": ["string", "null"]`). Not supported: `$ref`,
            `oneOf`, `allOf`, `not`, non-object/array roots, nesting beyond 10 levels. `anyOf` is supported.
        iso_8601:
          type: boolean
          description: date — output ISO 8601 (cannot combine with month/day/year/delimiter)
        month:
          type: string
          enum: [M, MM, MMM, MMMM]
          description: date — month format token
        day:
          type: string
          enum: [D, DD, Do]
          description: date — day format token
        year:
          type: string
          enum: [YYYY, YY]
          description: date — year format token
        delimiter:
          type: string
          description: date — single-character delimiter between date parts

    EnrichmentOutputColumnConfig:
      type: object
      description: |
        Per-column enrichment config. **Agent mode** (default): include `prompt` and `contexts`.
        **Tool-only mode**: set `tool` and its parameters (do not use `prompt`/`contexts`).
        `run_when` / `run_when_config` apply in both modes.
      properties:
        prompt:
          type: string
          description: Agent instructions for this column (agent mode)
        contexts:
          type: array
          description: Column headers used as input context (agent mode)
          items:
            type: string
        tools:
          type: array
          description: "Agent tools (agent mode): web_search, scrape, pdf, image, http_request, check_urls, text_search_pdf. Default when omitted: web_search, scrape, pdf, http_request. `[]` = reasoning only, no tools."
          items:
            type: string
        max_tool_calls:
          type: integer
          minimum: 0
          maximum: 10
          description: Agent mode — cap on tool calls per cell (default and max 10)
        format:
          type: string
          enum: [text, number, url, email, tag, date, json, boolean]
        format_details:
          $ref: "#/components/schemas/FormatDetails"
        run_when:
          type: string
          enum: [always, any_filled, all_filled, dynamic]
          description: |
            Per-row gate, checked before the cell runs (agent and tool-only columns). A skipped cell is
            left empty and costs 0 credits. `always` (default) runs every row. `any_filled` runs when at
            least one of the column's dependencies has a value — `contexts` for agent columns, column-mapped
            tool params for tool-only columns (recommended default for chained columns: it skips rows where
            the upstream column found nothing). `all_filled` runs only when every dependency has a value.
            `dynamic` evaluates `run_when_config` rules against cell values — use it to branch on an
            earlier output (e.g. run "CEO email" only when "CEO" is_not_empty, or skip an expensive
            tool-only lookup unless "Company Size" text_contains "enterprise").
        run_when_config:
          type: object
          description: |
            Rules for `run_when: dynamic` (required then, ignored otherwise). The column runs for a row
            only when the rules match: `match_mode: all` (default) = every rule matches, `any` = at least
            one. Agent columns: every `column` a rule tests must also be listed in this column's `contexts`
            — a rule on a column that is not a context sees an empty value. Tool-only columns: rule
            columns become dependencies automatically; no `contexts` needed.
          properties:
            match_mode:
              type: string
              enum: [all, any]
              description: How rules combine — `all` (default) or `any`
            rules:
              type: array
              minItems: 1
              description: Conditions evaluated against the row's cell values
              items:
                type: object
                properties:
                  column:
                    type: string
                    description: Header of an input column or an earlier output column (agent columns must also list it in `contexts`; tool-only columns need nothing extra)
                  condition:
                    type: string
                    enum:
                      [
                        is_empty,
                        is_not_empty,
                        text_contains,
                        text_does_not_contain,
                        text_starts_with,
                        text_ends_with,
                        text_is_exactly,
                      ]
                    description: |
                      `is_empty` / `is_not_empty` need no `value` ("not found" counts as empty). The `text_*`
                      conditions compare against `value`, case-insensitive, except `text_is_exactly` (exact,
                      case-sensitive).
                  value:
                    type: string
                    description: Comparison text for the `text_*` conditions (ignored for `is_empty` / `is_not_empty`)
                required: [column, condition]
          example:
            match_mode: all
            rules:
              - column: "CEO"
                condition: "is_not_empty"
              - column: "Company Size"
                condition: "text_contains"
                value: "enterprise"
        tool:
          type: string
          description: "Tool-only mode: scrape, web_search, pdf, image, code, LinkedIn tools, etc."
        url:
          type: string
          description: Column header or static URL (tool-only)
        query:
          type: string
          description: Column header or static query (tool-only, web_search)
        date_start:
          type: string
          description: "Optional start date for filtering search results. Format: YYYY-MM-DD (tool-only, web_search)"
        date_end:
          type: string
          description: "Optional end date filter. Format: YYYY-MM-DD. Defaults to today if date_start is provided (tool-only, web_search)"
        code:
          type: string
          description: JavaScript source (tool-only, code tool)
        args:
          type: object
          description: |
            Named arguments for the code tool (tool-only). Keys are names referenced in your JavaScript
            (e.g. `args.first`). Values are column headers (dynamic per row) or static strings.
        proxy_country_code:
          type: string
        wait_longer:
          type: boolean
        skip_cache:
          type: boolean
        delete:
          type: boolean
          description: PATCH /enrichments/{id} only — set true to remove this column

    EnrichmentOutputSpec:
      type: object
      description: |
        Keys are output column headers. Values are per-column configuration objects.
        Any output column name is allowed; see the per-column schema for all supported fields.
        Columns run in dependency order: a column may list earlier output columns in `contexts`,
        and `run_when` decides per row whether it runs at all.
      additionalProperties:
        $ref: "#/components/schemas/EnrichmentOutputColumnConfig"
      example:
        "Employee Count":
          prompt: "Find the number of employees at this company"
          contexts: ["Company Name", "Website"]
          format: "number"
        "Industry":
          prompt: "What industry is this company in?"
          contexts: ["Company Name"]
          format: "tag"
          format_details:
            options: ["SaaS", "Fintech", "Healthcare", "Other"]
        "CEO":
          prompt: "Find the CEO's full name"
          contexts: ["Company Name", "Website"]
        "CEO Email":
          prompt: "Find a work email address for this CEO"
          contexts: ["Company Name", "CEO"]
          format: "email"
          run_when: "dynamic"
          run_when_config:
            rules:
              - column: "CEO"
                condition: "is_not_empty"

    Extraction:
      type: object
      properties:
        id:
          type: string
          description: Extraction id (ext_...)
        name:
          type: string
        status:
          type: string
          enum: [discovering, ready, discovery_failed]
        app_url:
          type: string
          format: uri
        starting_url:
          type: string
          format: uri
        goal_description:
          type: string
        output_record_json_schema:
          type: [object, string, "null"]
          description: The record schema you provided
        required_keys:
          type: array
          items:
            type: string
        locked:
          type: boolean
          description: Locked plans can't be re-discovered
        validation_passing:
          type: [boolean, "null"]
          description: Whether the last discovery validation passed
        discovered_at:
          type: [string, "null"]
          format: date-time
        run_credits_required:
          type: number
          description: Credits charged per run

    Monitor:
      type: object
      properties:
        id:
          type: string
          description: Monitor id (mon_...)
        name:
          type: string
        enabled:
          type: boolean
        cadence:
          type: string
          enum: [daily, weekly, monthly]
        minute:
          type: integer
        hour:
          type: integer
        day_of_week:
          type: [integer, "null"]
        day_of_month:
          type: [integer, "null"]
        timezone:
          type: string
        webhook_url:
          type: [string, "null"]
          format: uri
        alert_rule:
          type: string
          enum: [each_run, only_on_change]
        output_format:
          type: string
          enum: [current_only, current_and_previous]
        next_run_at:
          type: [string, "null"]
          format: date-time
        schedule_summary:
          type: string
          description: Human-readable schedule (e.g. "Daily at 9:00 UTC")
        enrichment_id:
          type: string
        enrichment_name:
          type: string
        created_at:
          type: string
          format: date-time
        has_input:
          type: boolean
          description: Whether the monitor carries fixed input data

    ScrapeResponse:
      type: object
      description: Synchronous scrape result (legacy request_status format — endpoint unchanged from the legacy API).
      properties:
        request_status:
          type: string
          enum: [success]
        text:
          type: string
          description: The extracted text content from the webpage
        url:
          type: string
          format: uri
          description: The URL that was scraped
        base_url_for_links:
          type: string
          description: The base URL for resolving relative links
        status_code:
          type: integer
          description: The HTTP status code returned by the server
        possibly_blocked:
          type: boolean
          description: Present when the page may be blocked by anti-scraping measures
        credit_used:
          type: number
          description: The number of credits consumed
        credit_detail:
          $ref: "#/components/schemas/CreditDetail"
        riveter_app_link:
          type: string
          format: uri
          description: Direct link to view this scrape in the Riveter application

    Account:
      type: object
      properties:
        uuid:
          type: string
          format: uuid
          description: Unique identifier for the account
        name:
          type: string
          description: Account name
        plan:
          type: string
          enum: [free, starter, advanced, pro, enterprise]
          description: Current billing plan
        credit:
          $ref: "#/components/schemas/Credit"
      required:
        - uuid
        - name
        - plan
        - credit

    Credit:
      type: object
      properties:
        count:
          type: integer
          description: Current credit count
        max:
          type: integer
          description: Maximum credits available
        balance:
          type: integer
          description: Remaining credit balance
      required:
        - count
        - max
        - balance

    ApiKeyInfo:
      type: object
      properties:
        name:
          type: string
          description: Name of the API key
        last_used_at:
          type: [string, "null"]
          format: date-time
          description: When the API key was last used
        created_by:
          $ref: "#/components/schemas/User"
      required:
        - name
        - last_used_at
        - created_by

    User:
      type: object
      properties:
        uuid:
          type: string
          format: uuid
          description: User's unique identifier
        name:
          type: string
          description: User's full name
        email:
          type: string
          format: email
          description: User's email address
      required:
        - uuid
        - name
        - email

    Error:
      type: object
      description: The uniform error body (all endpoints except 401 auth failures).
      properties:
        error:
          type: object
          properties:
            type:
              type: string
              description: Machine-readable error type (bad_request, not_found, forbidden, feature_not_enabled, duplicate_run_key, insufficient_credits, credit_cap_exceeded, validation, not_implemented, ...)
            message:
              type: string
              description: Human-readable explanation
            details:
              description: Optional extra context (e.g. per-field validation errors)
            credit_detail:
              $ref: "#/components/schemas/CreditDetail"
              description: On insufficient_credits and credit_cap_exceeded — the estimate plus credits_remaining and sufficient_credits
          required:
            - type
            - message
      required:
        - error
      examples:
        - error:
            type: "not_found"
            message: "No run found with id run_018f6a70-..."
        - error:
            type: "credit_cap_exceeded"
            message: "This run could cost up to 1920 credits, more than the max_credits of 1000"
            credit_detail:
              estimate:
                minimum: 0
                maximum: 1920
                charged_upfront: false
              credits_used: 0
              credits_remaining: 5000
              sufficient_credits: true

    AuthError:
      type: object
      description: Authentication failures come from the shared auth layer and use the legacy shape.
      properties:
        request_status:
          type: string
          enum: [error]
        message:
          type: string
        error_type:
          type: string
          enum: [unauthorized]
      example:
        request_status: "error"
        message: "Invalid or missing API key"
        error_type: "unauthorized"

    # ----- dataset engine -----

    OutputColumnSpec:
      type: object
      description: One output column of an enrichment. A plain string is accepted in its place (a column name with no prompt or schema).
      properties:
        name:
          type: string
          maxLength: 100
          description: The column name; the handle in every later call
        prompt:
          type: string
          maxLength: 5000
          description: What to find for this column, on top of the enrichment's task prompt
        output_schema:
          type: object
          description: 'A JSON Schema for the cell value (`help` with `output_schema`). Omitted, a plain-value type is inferred; `{ "type": "string" }` keeps free text.'
      required:
        - name

    OutputColumnOperations:
      type: object
      description: |
        Output column edits keyed by column name. A name not on the enrichment adds a column; `prompt` / `output_schema` change one (its filled cells read `stale`); `rename` renames it (cells kept); `delete: true` removes it.
      additionalProperties:
        type: object
        properties:
          prompt:
            type: string
            maxLength: 5000
            description: What to find for this column
          output_schema:
            type: object
            description: 'A JSON Schema for the cell value (`help` with `output_schema`). Omitted on an added column, a plain-value type is inferred; `{ "type": "string" }` keeps free text.'
          rename:
            type: string
            maxLength: 100
            description: The column's new name
          delete:
            type: boolean
            description: Remove the column

    InputColumnOperations:
      type: object
      description: |
        Input column edits keyed by column name. `{}` adds the column; `rename` renames it; `delete: true` removes it (not the index column). Rows are untouched: an added column reads empty, a removed column's values are dropped from reads.
      additionalProperties:
        type: object
        properties:
          rename:
            type: string
            maxLength: 100
            description: The column's new name
          delete:
            type: boolean
            description: Remove the column

    Enrichment:
      type: object
      description: A dataset-engine enrichment (the recipe a fill runs)
      properties:
        id:
          type: string
          description: enr_...
        name:
          type: string
          nullable: true
        prompt:
          type: string
          description: The task prompt the row agent reads for every row
        input_columns:
          type: array
          items:
            type: string
          description: The headers rows must carry (empty = any)
        output:
          type: array
          items:
            $ref: "#/components/schemas/OutputColumnSpec"
        tools:
          type: array
          items:
            type: string
          description: The tools the row agent may call (names from GET /tools)
        budget_credits_per_row:
          type: number
          description: The most the row agent may spend on one row. Moves by 1.5 per output column added or removed unless `budget_locked`
        budget_locked:
          type: boolean
          description: true when the budget stays as set through column changes
        version:
          type: integer
          description: Goes up by one on every column change; a cell filled under an older version reads `stale`
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      example:
        id: enr_018f5b60-1234-7abc-89ab-0123456789ab
        name: Company basics
        prompt: Research each company from its name and website.
        input_columns: [Company, Website]
        output:
          - name: CEO
          - name: Employee count
            prompt: Current headcount as an integer
            output_schema: { type: integer, minimum: 0 }
        tools: [web_search, scrape, pdf, http_request, code]
        budget_credits_per_row: 4
        budget_locked: false
        version: 1
        created_at: "2026-10-01T12:00:00Z"
        updated_at: "2026-10-01T12:00:00Z"

    DatasetRunSummary:
      type: object
      description: A run on a dataset, as the dataset lists it (the full run is GET /runs/{id})
      properties:
        id:
          type: string
          description: run_...
        type:
          type: string
          enum: [dataset_build, enrichment]
          description: dataset_build = list generation; enrichment = a fill
        status:
          type: string
        created_at:
          type: string
          format: date-time
        finished_at:
          type: string
          format: date-time
          nullable: true

    Dataset:
      type: object
      properties:
        id:
          type: string
          description: ds_...
        name:
          type: string
          nullable: true
        input_columns:
          type: array
          items:
            type: string
          description: Input column names, in order (row identity)
        index_column:
          type: string
          nullable: true
          description: The input column rows are unique on
        enrichment_id:
          type: string
          nullable: true
          description: The bound enrichment (its output columns are the dataset's)
        output_columns:
          type: array
          items:
            type: string
          description: Output column names, in order (from the bound enrichment; [] without one). Prompts and schemas are on GET /enrichments/{enrichment_id}
        row_count:
          type: integer
        rows_expired:
          type: boolean
          description: True once the rows were removed by retention; the structure stays
        import_in_progress:
          type: boolean
          description: A CSV import accepted with 202 has not landed yet
        runs:
          type: array
          items:
            $ref: "#/components/schemas/DatasetRunSummary"
          description: The five newest runs on the dataset
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      example:
        id: ds_018f6a70-1234-7abc-89ab-0123456789ab
        name: Targets
        input_columns: [Company, Website]
        index_column: Company
        enrichment_id: enr_018f5b60-1234-7abc-89ab-0123456789ab
        output_columns: [CEO, HQ city]
        row_count: 2
        rows_expired: false
        import_in_progress: false
        runs:
          - id: run_018f6a70-cccc-7abc-89ab-0123456789ab
            type: enrichment
            status: success
            created_at: "2026-10-01T12:05:00Z"
            finished_at: "2026-10-01T12:07:00Z"
        created_at: "2026-10-01T12:00:00Z"
        updated_at: "2026-10-01T12:07:00Z"

    DatasetListItem:
      type: object
      properties:
        id:
          type: string
          description: ds_...
        name:
          type: string
          nullable: true
        input_column_count:
          type: integer
        output_column_count:
          type: integer
        row_count:
          type: integer
        rows_expired:
          type: boolean
        newest_run:
          allOf:
            - $ref: "#/components/schemas/DatasetRunSummary"
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    DatasetCell:
      type: object
      properties:
        value:
          description: The cell value (a string, or structured JSON when the column has an output_schema); null until filled or when nothing was found
          nullable: true
        state:
          type: string
          enum: [pending, running, ok, not_found, failed, manual, stale]
          description: |
            pending = never filled; running = a fill is on it; ok / not_found / failed = the last fill's result
            (a failed cell never finished — the next fill re-runs it without force; a not_found cell is an answer and stays);
            manual = set by hand (PATCH rows/{row_id}); stale = filled before the column's prompt or schema changed (the next fill re-runs it)
        reason:
          type: string
          enum: [credit_limit, provider, internal]
          description: |
            Failed cells only. credit_limit = the account's credit limit refused the row's charge; provider = the model
            provider answered with an error or did not answer (run it again); internal = something went wrong on Riveter's
            side (run it again). A stopped run leaves no failed cells: its recorded values land as ok, the rest stay as they were.
        error:
          type: string
          description: Failed cells only. One fixed sentence for the `reason`, fit to show a user
        sources:
          type: array
          items:
            type: string
          description: URLs the agent cited, when present

    DatasetRow:
      type: object
      properties:
        id:
          type: string
          description: row_...
        position:
          type: integer
          description: Insertion order, from 1 (the row number a user sees); stable, and the paging cursor
        input:
          type: object
          additionalProperties:
            type: string
            nullable: true
          description: Input values by column name
        cells:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/DatasetCell"
          description: Cells by output column name (every output column is present; in a run's `output.rows`, only the columns the fill covered)
        credits:
          type: number
          description: Credits fills have charged for this row so far (each row run counted once)
        not_found_reason:
          type: string
          nullable: true
          description: The agent's one sentence on what it could not find on this row and why, from its last successful run; null when it found every column or has not run
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      example:
        id: row_018f6a70-aaaa-7abc-89ab-0123456789ab
        position: 1
        input: { Company: Apple, Website: apple.com }
        cells:
          CEO:
            {
              value: Tim Cook,
              state: ok,
              sources: ["https://www.apple.com/leadership/"],
            }
          HQ city: { value: null, state: pending }
        credits: 1.2
        not_found_reason: null
        created_at: "2026-10-01T12:00:00Z"
        updated_at: "2026-10-01T12:07:00Z"

    DatasetImportResult:
      type: object
      properties:
        import:
          type: object
          properties:
            status:
              type: string
              enum: [done, importing]
            rows:
              type: integer
              description: Rows in the file
            rows_added:
              type: integer
              nullable: true
              description: Null while importing
            skipped_duplicates:
              type: integer
              nullable: true
              description: Null while importing
        row_count:
          type: integer
          description: The dataset's row count after this request (staged rows included)

    ToolCatalog:
      type: object
      properties:
        tools:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
                description: The name to use in only_tools / additional_tools
              description:
                type: string
              credits_per_call:
                type: number
              price_note:
                type: string
                nullable: true
                description: When the price is not flat — the scrape settles to its outcome, text_search_pdf to the PDF's page count; the LinkedIn email / phone tools are charged only when the call returns a contact. Null for a flat price.
              default:
                type: boolean
                description: On without being named
              requires_opt_in:
                type: boolean
                description: Spends third-party credits; a Riveter admin enables it on the account
              enabled:
                type: boolean
                description: Usable on this account (opt-in satisfied or not needed; says nothing about the API key)
              requires_api_key:
                type: boolean
                description: Calls its provider with the account's own API key (Apollo, Hunter, Findymail, curl, USPTO); refused until one is linked under Settings > API keys
              api_key_linked:
                type: boolean
                nullable: true
                description: Whether the account has linked a key for this tool; null when the tool needs none
              companions:
                type: array
                items:
                  type: string
                description: Free helper tools this tool brings along
        budget:
          type: object
          properties:
            default_credits_per_output_column:
              type: number
              description: A new enrichment's budget per row is this times its output columns; it moves by this much per column added or removed until locked
            min_credits_per_row:
              type: number
            max_credits_per_row:
              type: number

  responses:
    BadRequest:
      description: The request is malformed or names a conflicting parameter combination
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Missing or invalid API key (legacy-shaped body — see the AuthError schema)
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/AuthError"
    Forbidden:
      description: The API key's account may not access this resource or endpoint (`forbidden`), or the datasets beta is not on for the account (`feature_not_enabled` — email support@riveterhq.com)
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: No resource with that id on this account
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Conflict:
      description: Duplicate run_key or a run already in progress
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    UnprocessableEntity:
      description: The request is valid but can't be executed (e.g. insufficient_credits, credit_cap_exceeded when max_credits is set, validation failure)
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

tags:
  - name: Runs
    description: |
      The uniform lifecycle for every async operation — status, results, stop.

      **The run** (returned by kickoffs and every `/runs` endpoint):

      ```json
      {
        "id": "run_018f6a70-1234-7abc-89ab-0123456789ab",
        "type": "enrichment",
        "status": "processing",
        "progress": {
          "percent_complete": 40,
          "estimated_seconds_remaining": 90,
          "elapsed_seconds": 60,
          "completed_cells": 4,
          "total_cells_expected": 10,
          "not_found_cells": 0
        },
        "credits_used": 2.5,
        "credit_detail": {
          "estimate": { "minimum": 0, "maximum": 10, "charged_upfront": false },
          "credits_used": 2.5
        },
        "app_url": "https://app.riveterhq.com/runs/run_018f6a70-...",
        "result_url": "https://api.riveterhq.com/v1/runs/run_018f6a70-.../result",
        "started_at": "2026-01-15T12:00:00Z",
        "finished_at": null,
        "error": null,
        "enrichment_id": "enr_018f5b60-...",
        "enrichment_name": "My Enrichment"
      }
      ```

      Related resource ids (`enrichment_id`, `dataset_id`, `extraction_id`, `monitor_id`) appear when they apply to the run. Kickoff responses may carry a few extra fields (e.g. `dataset_id`, `max_items` on dataset builds).
  - name: Enrich
    description: Enrich data and manage saved enrichment configurations
  - name: List builds
    description: The list builder — rows Riveter finds for a description (identifier columns only, researched by an enrichment afterwards), more rows for a finished build, rows shaped for a saved enrichment, and configured list builds. "Build a list" / "List builds" in the app.
  - name: Extractions
    description: Reusable site scrape/extract recipes and their runs
  - name: Tools
    description: Synchronous and quick utilities — web search and scraping.
  - name: Monitors
    description: Scheduled enrichment runs with webhooks
  - name: Account
    description: Account and API key information
  - name: Datasets (beta)
    description: Grids Riveter keeps for you — rows you send or Riveter generates, filled column by column by an enrichment ([POST /enrich](#tag/enrich/post/enrich) with `dataset_id`). In beta, turned on per account. Start with [POST /datasets](#tag/datasets-beta/post/datasets) — what a dataset is, and three worked flows.