{"openapi":"3.2.0","info":{"title":"Riveter API","description":"## Overview\n\nThe Riveter API lets you **build datasets**, **enrich** them, **scrape** webpages, quickly **search** the web, and **extract** data from complicated websites programmatically.\n\n- **A dataset** is a table in Riveter — input columns for what each row is (rows you send, or rows Riveter generates from a description), output columns an enrichment fills. Start at [POST /datasets](#tag/datasets/post/datasets) in the [Datasets](#tag/datasets) section.\n- **An enrichment** fills in new columns for rows of input data 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. It runs on a dataset's rows — the preferred way: the rows and the filled cells are kept — or on rows you send inline for a one-off ([POST /enrich](#tag/enrich/post/enrich)).\n- **A list build** finds rows for a description (\"top 50 US SaaS companies\") — identifier columns only, which an enrichment then researches: [POST /datasets](#tag/datasets/post/datasets) with `prompt` + `count`, or [POST /datasets/{id}/rows/build](#tag/datasets/post/datasets/{id}/rows/build) for more rows in a dataset you have.\n- **A scrape** lets you turn a URL into easily parseable text.\n- **A quick_search** lets you quickly web search a query, and pull structured results with urls, titles, and snippets — synchronously, in one request.\n- **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.\n\nEvery 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`.\n\n## The run lifecycle\n\nEvery kickoff endpoint (`/enrich`, `/datasets`, `/extractions/{id}/runs`, ...) returns `201 Created` with a **run**. From there:\n\n| Request | What it does |\n|---------|--------------|\n| `GET /runs/{id}` | Check status (`pending` → `enqueued` → `processing` → `success` or `stopped`) |\n| `GET /runs/{id}/result?wait=30` | Fetch the output. `wait` long-polls up to 50 seconds; `output` is `null` until the run finishes |\n| `POST /runs/{id}/stop` | Stop a run early |\n\nPrefer 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.\n\nEvery kickoff and every `/runs` endpoint returns the same **run** — see the [Runs](#tag/runs) section for the full shape.\n\n## Legacy endpoints\n\nThis is the second generation of the Riveter API, and Datasets are its surface: a dataset holds the rows, its enrichment holds the columns, and every operation here works on them. The generation before — verb-style paths (`/run_new_enrichment`, `/run_status`, `/build_dataset`, ...), *legacy enrichments* built in the app's Enrichments page or from a standalone list build, and the resource paths that only reached those (`POST /datasets/{id}/extend`, `POST /enrichments/{id}/datasets`, `GET /enrichments/summary`) — keeps answering at the same paths and is documented in the [legacy API docs](./openapi.legacy.yaml) only. A legacy enrichment's own id still works on `/enrich`, `/enrichments/{id}`, `/monitors`, and `/runs` while its account has it; its dataset copy (`dataset_id` on [GET /enrichments](#tag/enrich/get/enrichments)) is where new work goes.\n\nCommon legacy → current mappings: `/run_new_enrichment` → `/enrich`, `/run_enrichment` → `/enrich` + `enrichment_id`, `/run_status` → `/runs/{id}`, `/run_data` → `/runs/{id}/result`, `/build_dataset` → `/datasets` with `prompt` + `count`, `/monitor_enrichment` → `/monitors` with `dataset_id`. The full table is in the [legacy API docs](./openapi.legacy.yaml) (\"Migrating to the current API\").\n\nLegacy responses say so themselves: the body carries `\"legacy\": { \"notice\", \"successor\", \"docs\" }` ([LegacyNotice](#model/legacynotice)) and the response has `Deprecation: true` plus a `Link` header (`rel=\"successor-version\"`). A current path answers with `legacy` when the request took a legacy mode — a legacy enrichment's id, the list-build body (`max_items`) on `POST /datasets`, a monitor on a legacy enrichment — and `legacy.successor` says what to send instead.\n\n## Webhooks\n\nPass `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`). A dataset's generation run takes `webhook_url` and its auto-fill `fill_webhook_url`. Your endpoint should return a 2xx. Transient failures (network errors, 5xx, 408, 425, 429) are retried up to 2 times; any other 4xx response is treated as a rejection of the payload and is not retried.\n\nThe webhook payload shape is shared with the legacy API — see the [legacy API docs](./openapi.legacy.yaml) (\"Webhooks\" section) for the full payload reference.\n\n## Authentication\n\nAll endpoints require an API key via the Authorization header:\n```\nAuthorization: Bearer YOUR_API_KEY\n```\n[Get an API key here](https://app.riveterhq.com/settings/api)\n\n## Rate limiting\n\nDefault: 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`).\n\n## MCP server\n\nUse Riveter from Claude, ChatGPT, Cursor, or any MCP-compatible AI assistant. Pick one of the two ways to connect.\n\n### Hosted server (claude.ai, Claude Desktop, Cowork, mobile, ChatGPT, Claude Code, Cursor)\n\nAdd `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.\n\n**Claude Code:**\n```bash\nclaude mcp add --transport http riveter https://mcp.riveterhq.com/mcp\n```\nThen run `/mcp` inside Claude Code and choose Authenticate.\n\n**Cursor** — paste into your MCP config, then click Connect in the MCP settings:\n```json\n{\n  \"mcpServers\": {\n    \"riveter\": {\n      \"url\": \"https://mcp.riveterhq.com/mcp\"\n    }\n  }\n}\n```\n\nIf 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\" }`).\n\n### Local server (npx)\n\nRuns 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.\n\n**Claude Code:**\n```bash\nclaude mcp add riveter \\\n  --env RIVETER_API_KEY=YOUR_API_KEY \\\n  -- npx -y --prefer-online riveter-mcp-server@latest\n```\n\n**Codex:**\n```bash\ncodex mcp add riveter \\\n  --env RIVETER_API_KEY=YOUR_API_KEY \\\n  -- npx -y --prefer-online riveter-mcp-server@latest\n```\n\n**Cursor / Windsurf / Claude Desktop** — paste into your MCP config:\n```json\n{\n  \"mcpServers\": {\n    \"riveter\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"--prefer-online\", \"riveter-mcp-server@latest\"],\n      \"env\": {\n        \"RIVETER_API_KEY\": \"YOUR_API_KEY\"\n      }\n    }\n  }\n}\n```\n\nBoth servers expose every API endpoint as a tool, with full descriptions and typed parameters. No setup beyond the API key.\n\n_**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._\n\n## Official SDKs\n\nThe SDKs handle auth, retries (429s and transient GET failures), the `wait` long-poll, polling until a run finishes (`wait_for_result`), and pagination. They cover every operation in this reference. The legacy generation ([the legacy API docs](?spec=legacy)) has no SDK method; call it over HTTP.\n\n| Language | Install | Package |\n|----------|---------|---------|\n| TypeScript / JavaScript | `npm install riveter-sdk` | [riveter-sdk on npm](https://www.npmjs.com/package/riveter-sdk) |\n| Python | `pip install riveter-sdk` | [riveter-sdk on PyPI](https://pypi.org/project/riveter-sdk/) (import `riveter`) |\n| Ruby | `gem install riveter-sdk` | [riveter-sdk on RubyGems](https://rubygems.org/gems/riveter-sdk) (`require \"riveter\"`) |\n| Go | `go get github.com/riveterhq/riveter-go` | [riveterhq/riveter-go](https://github.com/riveterhq/riveter-go) |\n\nEndpoints show the equivalent SDK call in all four languages next to the request example.\n\n## Timestamps\n\nAll timestamps in responses are ISO 8601 strings with second precision and an explicit UTC offset (e.g. `2026-01-15T12:00:00Z`).\n\n## Errors\n\nErrors use real HTTP status codes and a uniform body:\n\n```json\n{ \"error\": { \"type\": \"not_found\", \"message\": \"No run found with id run_...\" } }\n```\n\nCommon `type` values: `bad_request`, `not_found`, `forbidden`, `duplicate_run_key`, `insufficient_credits`, `credit_cap_exceeded`, `validation`, `not_implemented`. A dataset endpoint on an older account that has not completed its move to Datasets answers `403` with `feature_not_enabled`; the account moves itself from [Datasets](https://app.riveterhq.com/v2/datasets) in the app (**Migrate to Datasets**).\n\nOne exception: authentication failures (`401`) come from the shared auth layer and use the legacy shape `{ \"request_status\": \"error\", \"message\": \"...\", \"error_type\": \"unauthorized\" }`.\n\n## Credits: estimate before you spend\n\nEvery endpoint that spends credits (`/enrich`, `/datasets`, `/datasets/{id}/rows/build`, `/configured_datasets/{id}/build`, `/extractions`, `/extractions/{id}/runs`, `/monitors`, `/quick_search`, `/search_agent`, `/scrape`) accepts two optional fields:\n\n- `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`.\n- `max_credits` — a ceiling. When the estimate's `maximum` exceeds it, the request is refused with `422 credit_cap_exceeded` and nothing is charged.\n\n`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`.\n\nEvery run also carries `credit_detail` (`{ estimate: { minimum, maximum, charged_upfront, upfront?, expected?, typical? }, 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 a dataset build settles below its cap. `charged_upfront` is `true` for dataset builds and extractions (a deposit at kickoff) and `false` for enrichments and searches (accrued as work completes). A dataset build runs on a credit budget — `budget` on the request, else 50 + 1.5 per row — billed on what its agent spends within it (tokens plus searches, scrapes, PDFs). A deposit of `upfront` (= `expected`) is charged at kickoff; the rest is charged as it runs, up to the budget; unused deposit is refunded; a failed build refunds everything; `typical` is the recent p25–p75 range. A configured list build pays its template's flat tier price instead.\n\n`POST /monitors` charges nothing at creation; its estimate is the upper bound of **one** scheduled fill of the dataset's rows, and every scheduled run charges that again.\n","x-mcp-instructions":"Riveter builds lists (rows), enriches rows with new columns, scrapes pages, searches, and answers research questions.\nAsync 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`.\n`scrape`, `quick_search`, `search_agent` are synchronous. Monitors fill a dataset again on a schedule.\nTo enrich rows, put them in a dataset (`build_dataset` with `rows`; `ds_...` keeps rows and cells) and `enrich` with `dataset_id`; `list_dataset_rows` reads it. `help` → `datasets`.\nPaid tools accept `estimate_cost: true` (estimate only) and `max_credits` (refused above it).\nCall `help` before writing `output` columns or when an error names a limit.\nIds: `run_`, `enr_`, `ds_`, `row_`, `ext_`, `mon_`. Auth comes from the MCP connection.\n","x-mcp-help":{"overview":"# Riveter, in one page\n\nRiveter turns lists into researched tables.\n\n- **Dataset** (`build_dataset` with `rows` or `prompt` + `count`; `help` → `datasets`) — a table Riveter keeps (`ds_...`): rows you send or rows Riveter finds from a description, plus the cells an enrichment fills. The default for any list a user wants to keep, grow, or research.\n- **Enrichment** (`enrich`) — takes rows (a `dataset_id`, or inline `input`) and fills in new columns using AI agents with web search, scraping, PDFs, LinkedIn tools, and more. This is where research happens.\n- **List build** (`build_dataset` with `prompt` + `count`; `build_dataset_rows` for more rows in a dataset) — finds rows from a description into a dataset: identifier columns only (e.g. Company + Website), researched by nothing until `enrich` runs (`auto_fill: true` runs it as the rows land). `help` → `datasets_vs_enrichment`.\n- **Scrape** (`scrape`) — one URL → clean text. Synchronous.\n- **Quick search** (`quick_search`) — one query → titles, URLs, snippets. Synchronous.\n- **Search agent** (`search_agent`) — one question → one AI-researched answer. Synchronous. Same agent that fills one enrichment cell.\n- **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.\n- **Monitor** (`create_monitor`; `help` → `monitors`) — fill a dataset's columns again daily / weekly / monthly and report what changed, optionally to a webhook. Every scheduled run charges the fill price again.\n\n**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`.\n\n**Typical flows**\n1. User has a list, wants columns → `build_dataset` with `rows` + `attributes` (or a saved `enrichment_id`), then `enrich` with the `dataset_id` — the rows and cells are kept, later fills run only what is new (`help(datasets)`). For a one-off whose rows need not be kept, `enrich` with inline `input` and a column config (`help(enrich_modes)`).\n2. User has no list → `build_dataset` with `prompt` + `count` alone (a dataset; the identity columns and one typed output column per fact the prompt asks for are written from it; `attributes` names the columns yourself; `auto_fill: true` researches the rows as they land) → `enrich` with the `dataset_id` for any later fill.\n3. Repeatable research → build and tune the enrichment once, then always run it by `enrichment_id`.\n\n**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)`.\n","enrich_modes":"# `enrich`: where the rows come from, and the three ways to say what columns to add\n\nRows come from exactly one of `dataset_id` or `input`.\n\n**Preferred: `dataset_id`.** Put the rows in a dataset first — `build_dataset` with `rows` and the output columns as `attributes` (or a saved `enrichment_id`), free — then `enrich` with its `dataset_id`. The dataset's enrichment holds the columns, so no column config is sent; the rows and every filled cell are kept, a later call fills only what is new or changed, and `list_dataset_rows` reads the grid at any time. `prompt` + `attributes` on a dataset fills those columns, adding the missing ones; `output` is refused there. `help(datasets)`.\n```json\n{ \"dataset_id\": \"ds_018f6a70-1234-7abc-89ab-0123456789ab\" }\n```\n\n**`input`** is for a one-off: the rows come in the request and the results come back on the run only; nothing is kept. Columnar — keys are headers, values are equal-length string arrays, one position = one row:\n```json\n{ \"input\": { \"Company\": [\"Apple\", \"Google\"], \"Website\": [\"apple.com\", \"google.com\"] } }\n```\nWith `input`, column config comes from exactly one of the three below. Every inline run is the same engine as a dataset fill — one row agent researches a row's columns together — on a single-run dataset whose rows expire; the result is columnar on the run.\n\n## 1. `enrichment_id` — run a saved enrichment (most consistent)\nFixed, tested config → the most consistent results of the three. Up to 10,000 rows. `input` headers must match the enrichment's input columns (`get_enrichment` shows them under `input_columns`). Find ids with `list_enrichments`.\n```json\n{ \"enrichment_id\": \"enr_...\", \"input\": { \"Company Name\": [\"Acme Corp\", \"Tech Solutions Inc\"] } }\n```\n\n## 2. `prompt` + `attributes` — a task and column names\nThe `prompt` is the task the row agent follows for every row; `attributes` are the output column names (max 20), each typed from its name. Up to 1,000 rows. No setup; good for one-offs and for drafting an enrichment you will then save and tune.\n```json\n{\n  \"prompt\": \"For each B2B SaaS company, find the CEO, employee count, and primary industry.\",\n  \"attributes\": [\"CEO\", \"Employee Count\", \"Industry\"],\n  \"input\": { \"Company\": [\"Apple\", \"Google\"] }\n}\n```\nPut qualifiers in the prompt (industry, geography, entity type) — the agent sees the prompt and its row, nothing else.\n\n## 3. `output` — a spec per column\nPer column: `prompt` (the agent's instruction for that cell), optional `output_schema` (a JSON Schema for the value), optional `tool` (pin one tool: its one call per row is the cell). `contexts` is accepted and ignored — the row agent reads the whole row. Up to 1,000 rows. See `help(column_config)` for every field and `help(examples)` for complete recipes.\n```json\n{\n  \"input\": { \"Company\": [\"Apple\", \"Google\"] },\n  \"output\": {\n    \"Employee Count\": { \"prompt\": \"Find the current number of employees at this company.\", \"output_schema\": { \"type\": \"number\" } },\n    \"CEO\": { \"prompt\": \"Find the CEO's full name.\" }\n  }\n}\n```\nA column with `format`, `run_when`, `tools`, `max_tool_calls`, or tool parameters keeps the legacy pipeline (one agent per cell, legacy enrichment), and the response carries `legacy` naming the fields to drop.\n\n## Save, then run by id\nTo make mode 2 or 3 repeatable: save an enrichment with `create_enrichment` + `prompt` + `output` (or create one in the Riveter app), tune it with `update_enrichment`, then run it with mode 1. A saved enrichment comes with an empty dataset bound to it (`dataset_id` in the response): `add_dataset_rows` to it and `enrich` with the `dataset_id` keeps the rows and cells in the app instead.\n\n## After kickoff\nThe 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.\n","column_config":"# Output column config (the values in `output: { \"<Header>\": { ... } }`)\n\n## The dataset engine (every dataset, and inline `enrich` by default)\n| field | notes |\n|---|---|\n| `prompt` | What this cell is: one value. Self-contained; the row agent also has the task prompt and the whole row, so name the entity type and any qualifiers. Do not ask for the source URL, the reporting date, or the currency in the same cell: the agent records the URLs it cited on every cell's `sources`, and a second fact is a second column (\"ARR\" and \"ARR reporting date\"). A prompt that bundles two facts into one cell (\"phone number with source URL\") gets no type and free-text cells. Omit it and the column name is the instruction. |\n| `output_schema` | Optional JSON Schema for the value (`{ \"type\": \"number\" }`, an enum, an object). Inferred from the prompt when omitted: email, URL, LinkedIn URL, domain, date, year, count, number, money, percentage, boolean, phone, address, location, country, a person's or company's name, ticker; only prose and lists stay untyped. `help(output_schema)`. |\n| `tool` | Optional. Pin one tool by name (`list_tools`): its one call per row is the cell, arguments worked out by the agent. `help(pinned_columns)`. |\n| `contexts` | Accepted and ignored — the agent reads every column of the row. |\n\nTools, budget, and the task prompt belong to the enrichment, not to a column (`create_enrichment`, `update_dataset`). Columns are researched together by one agent per row and need no order.\n\n## The legacy pipeline (one agent per cell)\nA column that uses any field below — `format` / `format_details`, `run_when` / `run_when_config`, `tools`, `max_tool_calls`, or a `tool` with its parameters — runs on the legacy pipeline instead: a legacy enrichment, one agent or one tool call per cell. The response carries `legacy` naming the fields to drop. The rest of this topic documents those fields; two modes per column, never mixed.\n\n### Agent mode — an AI agent researches each cell\n| field | notes |\n|---|---|\n| `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\"). |\n| `contexts` | Required, non-empty. Headers whose values are given to the agent: input columns and/or EARLIER output columns. |\n| `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). |\n| `format` | Optional. `text` (default), `number`, `url`, `email`, `tag`, `date`, `json`, `boolean`. See `help(formats)`. |\n| `format_details` | Per-format options (tag options, JSON schema, ...). See `help(formats)`. |\n| `run_when` | Per-row gate. See `help(run_when)`. |\n| `max_tool_calls` | Optional cap on tool calls per cell (default 10). |\n\n### Tool-only mode — one tool, no agent loop; cheaper and deterministic\nSet `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.\n```json\n{ \"Homepage Text\": { \"tool\": \"scrape\", \"url\": \"Website\" } }\n```\nWhen 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)`.\n`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)`.\n\n### Ordering and dependencies\nColumns 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.\n\n### Rules that save credits and avoid bad data\n- 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.\n- Group related fields from one source into one `json` column instead of N agent columns.\n- Prefer `text` / `number` / `boolean` over `json` when one value is enough.\n- For URL columns add \"Only return a real URL found via search or scrape — do not guess.\"\n- Do not tell the agent to return null / N/A when not found — Riveter emits a \"not found\" value itself.\n- Chained columns: set `run_when: \"any_filled\"` so rows where the upstream column found nothing are skipped for free.\n- Input headers and output headers must all be unique.\n","run_when":"# `run_when` — skip rows, chain columns (legacy pipeline)\n\nA legacy-pipeline field: an `output` column that sets it runs on the legacy pipeline and the response carries `legacy` (`help(column_config)`). The dataset engine has no per-row gate; the row agent skips what the row cannot answer (`not_found`), and a fill re-runs only `pending` / `stale` / `failed` cells.\n\nA per-row gate checked before the cell runs, for agent columns AND tool-only columns. A skipped cell stays empty and costs **0 credits**.\n\n| value | runs when |\n|---|---|\n| `always` (default) | every row |\n| `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. |\n| `all_filled` | every dependency has a value. Rarely wanted — one \"not found\" silently skips the row. |\n| `dynamic` | the `run_when_config` rules match. Use to branch on an earlier output's VALUE. |\n\n## `run_when_config`\n```json\n{ \"match_mode\": \"all\", \"rules\": [{ \"column\": \"CEO\", \"condition\": \"is_not_empty\" }] }\n```\n- `match_mode`: `all` (default) or `any`.\n- `rules[].column`: header of an input column or an EARLIER output column.\n  - 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.\n  - 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).\n- `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`).\n\n## Example — skip an expensive tool-only lookup unless a cheap check passes\n```json\n{\n  \"input\": { \"LinkedIn URL\": [\"https://linkedin.com/in/example\"] },\n  \"output\": {\n    \"Profile\": { \"tool\": \"linkedin_person_profile\", \"url\": \"LinkedIn URL\" },\n    \"Seniority\": { \"prompt\": \"Classify this person's seniority from their profile.\", \"contexts\": [\"Profile\"], \"format\": \"tag\", \"format_details\": { \"options\": [\"ic\", \"manager\", \"executive\"] }, \"run_when\": \"any_filled\" },\n    \"Work Email\": {\n      \"tool\": \"linkedin_person_contact\",\n      \"url\": \"LinkedIn URL\",\n      \"run_when\": \"dynamic\",\n      \"run_when_config\": { \"rules\": [{ \"column\": \"Seniority\", \"condition\": \"text_is_exactly\", \"value\": \"executive\" }] }\n    }\n  }\n}\n```\n`Work Email` only spends credits on rows where `Seniority` is `executive`.\n\n## Example — look up an email only when a CEO was found; write notes only for enterprise companies\n```json\n{\n  \"input\": { \"Company\": [\"Stripe\", \"Acme Corp\"] },\n  \"output\": {\n    \"CEO\": { \"prompt\": \"Find the CEO's full name.\", \"contexts\": [\"Company\"] },\n    \"Company Size\": { \"prompt\": \"Classify this company's size.\", \"contexts\": [\"Company\"], \"format\": \"tag\", \"format_details\": { \"options\": [\"startup\", \"mid-market\", \"enterprise\"] } },\n    \"CEO Email\": {\n      \"prompt\": \"Find a work email address for this CEO.\",\n      \"contexts\": [\"Company\", \"CEO\"],\n      \"format\": \"email\",\n      \"run_when\": \"dynamic\",\n      \"run_when_config\": { \"rules\": [{ \"column\": \"CEO\", \"condition\": \"is_not_empty\" }] }\n    },\n    \"Enterprise Notes\": {\n      \"prompt\": \"Summarize this company's enterprise offering, pricing model, and notable enterprise customers.\",\n      \"contexts\": [\"Company\", \"Company Size\"],\n      \"run_when\": \"dynamic\",\n      \"run_when_config\": { \"rules\": [{ \"column\": \"Company Size\", \"condition\": \"text_is_exactly\", \"value\": \"enterprise\" }] }\n    }\n  }\n}\n```\nWith `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`.\n","formats":"# `format` and `format_details` (legacy pipeline)\n\nLegacy-pipeline fields: an `output` column that sets them runs on the legacy pipeline and the response carries `legacy`. On the dataset engine the value's type is `output_schema` — `{ \"type\": \"number\" }`, `{ \"type\": \"string\", \"format\": \"date\" }`, an `enum`, an object (`help(output_schema)`).\n\nOnly set a format when the value has a natural type. `text` is the default and is fine for prose.\n\n| format | value | `format_details` keys |\n|---|---|---|\n| `text` | free text | — |\n| `number` | numeric | `decimal_places` (0–9), `currency_code` (\"USD\"), `commas` (bool), `percentage` (bool). `currency_code` and `percentage` are mutually exclusive. |\n| `url` | one URL | — |\n| `email` | one email | — |\n| `boolean` | true/false | — |\n| `tag` | one of a fixed set | `options` (required, string array), `allow_multiple` (bool), `descriptions` (map option → meaning) |\n| `date` | a date | `iso_8601: true`, or tokens `month` (M, MM, MMM, MMMM), `day` (D, DD, Do), `year` (YYYY, YY), `delimiter` |\n| `json` | structured object | `schema` (JSON Schema object) and/or `description` (natural-language shape) |\n\n## `tag` example\n```json\n{ \"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\" } } } }\n```\n\n## `json` schema rules\n- 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.\n- Optional fields: make them nullable, `\"type\": [\"string\", \"null\"]`.\n- Add `minLength` to text fields you care about (reasoning ~50, descriptions ~20, names ~2) — `required` alone allows `\"\"`.\n- Keep it flat: ≤ 5–6 properties per column. Split otherwise.\n- Not supported: `$ref`, `oneOf`, `allOf`, `not`, nesting > 10 levels. `anyOf` is supported. Do not use `maxItems` on arrays (it truncates data).\n```json\n{\n  \"Pricing\": {\n    \"prompt\": \"Find this product's pricing. Return the plan names, monthly price in USD, and whether a free tier exists.\",\n    \"contexts\": [\"Product\", \"Website\"],\n    \"format\": \"json\",\n    \"format_details\": {\n      \"schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"plans\": { \"type\": \"array\", \"items\": { \"type\": \"object\", \"properties\": { \"name\": { \"type\": \"string\", \"minLength\": 2 }, \"monthly_usd\": { \"type\": [\"number\", \"null\"] } }, \"required\": [\"name\", \"monthly_usd\"], \"additionalProperties\": false } },\n          \"has_free_tier\": { \"type\": \"boolean\" },\n          \"source_url\": { \"type\": [\"string\", \"null\"] }\n        },\n        \"required\": [\"plans\", \"has_free_tier\", \"source_url\"],\n        \"additionalProperties\": false\n      }\n    }\n  }\n}\n```\n","tools":"# Tools\n\nOn the dataset engine tools belong to the enrichment (`only_tools` / `additional_tools` on `create_enrichment` and `update_enrichment`; `list_tools` names them with prices and whether the account may use each), and a column pins one with `tool` (`help(pinned_columns)`). The per-column fields below are the legacy pipeline's; an `output` column that sets `tools`, `max_tool_calls`, or tool parameters runs there and the response carries `legacy`.\n\n## Agent-mode `tools` (the agent decides when to call them)\n`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.\n\n## Tool-only `tool` (one deterministic call per row, no agent)\nParam values that equal a column header are per-row; anything else is static.\n\n| `tool` | required params | optional | returns |\n|---|---|---|---|\n| `scrape` | `url` | `proxy_country_code`, `wait_longer`, `skip_cache` | page text |\n| `web_search` | `query` | `date_start`, `date_end` (YYYY-MM-DD) | results with titles/urls/snippets |\n| `pdf` | `url`, `prompt` | — | answer extracted from the PDF |\n| `image` | `url`, `prompt` | — | answer about the image |\n| `http_request` | `url` | `method` (GET/POST), body fields | raw HTTP response (public APIs only) |\n| `code` | `code` | `args` | return value of your JS. Free (0 credits). Sandbox, no network. |\n| `linkedin_person_profile` | `url` (linkedin.com/in/<slug>) | — | profile data |\n| `linkedin_person_contact` | `url` | — | emails (waterfall) |\n| `linkedin_person_contact_lite` / `linkedin_person_phones_lite` | `url` | — | cheaper email / phone lookups |\n| `linkedin_person_posts` / `_comments` / `_reactions` / `_activities` | `url` | — | activity |\n| `linkedin_company_profile` / `_data` / `_job_count` / `_depth_chart` / `_posts` | `url` (linkedin.com/company/<slug>) | — | company data |\n| `linkedin_post_comments` / `linkedin_post_reactions` | `post_url` | `reaction_type` | post engagement |\n\n## `code` tool\n`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.\n```json\n{\n  \"Domain\": { \"tool\": \"code\", \"code\": \"try { return new URL(args.url).hostname.replace(/^www\\\\./, '') } catch (e) { return null }\", \"args\": { \"url\": \"Website\" } },\n  \"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\" } }\n}\n```\n\n## LinkedIn: always resolve the URL first\nInput 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\".\n\n## When to use which mode\n- Known URL + one tool (read a PDF, scrape a page, LinkedIn lookup) → tool-only.\n- Deterministic transform of existing columns → tool-only `code`.\n- Needs searching, reasoning, or more than one tool → agent mode.\n\n## On a dataset\nA dataset's output column pins a tool with `\"tool\": \"<name>\"`: the row agent makes one call of that tool for the row — with arguments it works out itself (the column `prompt` says what to call it on) — and the call's result is the cell. See `help` → `pinned_columns`.\n","examples":"# Complete `enrich` payloads (copy, edit headers, run)\n\nEach block is a full tool call with inline `input`; the same `output` entries are `attributes` / `output` on `build_dataset` or `create_enrichment` when the rows should be kept. One row agent researches a row's columns together, so columns need no order and no `contexts`; typed values use `output_schema` (`help(output_schema)`), one tool's output uses `tool` (`help(pinned_columns)`).\n\n## Company basics\n```json\n{\n  \"input\": { \"Company\": [\"Stripe\", \"Notion\"], \"Website\": [\"stripe.com\", \"notion.so\"] },\n  \"output\": {\n    \"Legal Name\": { \"prompt\": \"The company's registered legal name.\" },\n    \"HQ City\": { \"prompt\": \"The city of the company's headquarters.\" },\n    \"Employee Count\": { \"prompt\": \"The current number of employees.\", \"output_schema\": { \"type\": \"integer\", \"minimum\": 0 } },\n    \"Annual Revenue\": { \"prompt\": \"The most recent annual revenue.\", \"output_schema\": { \"type\": \"number\", \"description\": \"Amount in USD; convert when the source reports another currency\" } }\n  }\n}\n```\n\n## Lead scoring (extract → analyze → score)\n```json\n{\n  \"input\": { \"Company\": [\"Acme Agency\"], \"Website\": [\"acme.agency\"] },\n  \"output\": {\n    \"Services Offered\": { \"prompt\": \"The services this agency offers, from its website.\" },\n    \"Is Webflow Agency\": { \"prompt\": \"Is this primarily a Webflow design/development agency, judging by the services offered?\", \"output_schema\": { \"type\": \"boolean\" } },\n    \"ICP Score\": {\n      \"prompt\": \"Fit for our ICP (Webflow agencies, 5–50 staff, US/EU): a score 0–100, a segment, and the reasoning.\",\n      \"output_schema\": { \"type\": \"object\", \"properties\": { \"score\": { \"type\": \"number\", \"minimum\": 0, \"maximum\": 100 }, \"segment\": { \"type\": \"string\", \"enum\": [\"ideal\", \"possible\", \"poor\"] }, \"reasoning\": { \"type\": \"string\" } }, \"required\": [\"score\", \"segment\", \"reasoning\"] }\n    }\n  }\n}\n```\n\n## Person enrichment via LinkedIn (find the URL → pinned tools → synthesis)\n```json\n{\n  \"input\": { \"Person Name\": [\"Tobias Lütke\", \"Patrick Collison\"], \"Qualifying Information\": [\"CEO of Shopify\", \"CEO of Stripe\"] },\n  \"output\": {\n    \"LinkedIn URL\": { \"prompt\": \"The person's canonical LinkedIn profile URL, https://linkedin.com/in/<slug>. Only a real URL found via search or scrape — never guessed.\", \"output_schema\": { \"type\": \"string\", \"format\": \"uri\" } },\n    \"LinkedIn Data\": { \"prompt\": \"The profile at the row's LinkedIn URL.\", \"tool\": \"linkedin_person_profile\" },\n    \"Contact Data\": { \"prompt\": \"Contact details for the profile at the row's LinkedIn URL.\", \"tool\": \"linkedin_person_contact\" },\n    \"Bio\": { \"prompt\": \"A two-sentence professional bio of the person, from the LinkedIn Data column.\" }\n  }\n}\n```\nFor companies: same shape with a `/company/<slug>` URL column and `linkedin_company_profile`.\n\n## PDF extraction (find the document → read it)\n```json\n{\n  \"input\": { \"Company\": [\"Apple\"] },\n  \"output\": {\n    \"Annual Report URL\": { \"prompt\": \"The URL of the company's most recent annual report PDF (10-K or equivalent). Only a real URL found via search.\", \"output_schema\": { \"type\": \"string\", \"format\": \"uri\" } },\n    \"Reported Revenue\": { \"prompt\": \"Ask the PDF at the Annual Report URL: total net revenue for the most recent fiscal year, as a number in USD.\", \"tool\": \"pdf\" }\n  }\n}\n```\nIf the PDF URL is already an input column, skip the first column.\n\n## Product matching across retailers (one object column per retailer)\n```json\n{\n  \"input\": { \"Product\": [\"Sony WH-1000XM5\"], \"Brand\": [\"Sony\"] },\n  \"output\": {\n    \"Amazon\": {\n      \"prompt\": \"Whether this exact product is sold on amazon.com: match type, URL, list price, sale price, stock status, and review rating.\",\n      \"output_schema\": { \"type\": \"object\", \"properties\": { \"match_type\": { \"type\": \"string\", \"enum\": [\"exact\", \"similar\", \"not_found\"] }, \"url\": { \"type\": \"string\", \"format\": \"uri\" }, \"list_price\": { \"type\": \"number\" }, \"sale_price\": { \"type\": \"number\" }, \"in_stock\": { \"type\": \"boolean\" }, \"review_rating\": { \"type\": \"number\" } }, \"required\": [\"match_type\", \"url\", \"list_price\", \"sale_price\", \"in_stock\", \"review_rating\"] }\n    }\n  }\n}\n```\n\n## Image analysis (URL known → pinned tool)\n```json\n{\n  \"input\": { \"Company\": [\"Stripe\"], \"Logo URL\": [\"https://stripe.com/img/v3/home/social.png\"] },\n  \"output\": { \"Logo Colors\": { \"prompt\": \"The dominant colors of the image at the Logo URL, as hex codes.\", \"tool\": \"image\" } }\n}\n```\n\n## Transform with code (the agent writes the code)\n```json\n{\n  \"input\": { \"First Name\": [\"Ada\"], \"Last Name\": [\"Lovelace\"], \"Website\": [\"https://www.example.com/about\"] },\n  \"output\": {\n    \"Full Name\": { \"prompt\": \"First Name and Last Name joined with a space.\", \"tool\": \"code\" },\n    \"Domain\": { \"prompt\": \"The hostname of the Website URL without a leading www.\", \"tool\": \"code\" }\n  }\n}\n```\n","credits":"# Credits\n\nEvery paid tool accepts two optional fields:\n- `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\" } }`.\n- `max_credits` — ceiling. If the estimate's `maximum` exceeds it, the call is refused with `credit_cap_exceeded` and nothing is charged.\n\nEvery 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) or a dataset build settles below its cap. `charged_upfront` is true for dataset builds and extractions (a deposit at kickoff; unused refunded), false for enrichments and searches (accrued as work completes). A dataset build's `upfront` is the deposit; `typical` is the recent p25–p75 range.\n\n**Estimate or run?** Pass `max_credits` on the call itself to run when the estimate fits, else refuse (`credit_cap_exceeded`) with the estimate in `credit_detail`. Use `estimate_cost: true` when asking the user first. When you ask them to proceed: for a dataset build say \"Charged now: N. At most: M.\" (`upfront` / `maximum` — `upfront` is taken at kickoff, not a soft estimate); for a fill or search say \"Up to N.\" When they say to run without a cost check, run without `max_credits`; `insufficient_credits` is the backstop.\n\n**Rough pricing** (use `estimate_cost` for exact numbers): a dataset fill charges each row what its agent spent within the enrichment's `budget_credits_per_row` — the model's tokens at Riveter's rate plus its tool calls at the `list_tools` prices (`price_note` marks the ones that settle after the call: `pdf` by the tokens its model spends reading the PDF, about 0.22 a page plus 0.44 for the answer, booked at 2; `image` likewise, about 0.2 for a typical image, booked at 0.4; `text_search_pdf` per page; the LinkedIn email / phone tools only on a contact). On the legacy pipeline an agent-mode cell = 1 credit and tool-only cells are priced per tool (scrape ≈ 0.10, web_search 0.04, pdf 2, text_search_pdf 2 + 0.5 per page, 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 runs on a credit budget (`budget` on the request, else 50 + 1.5 per row) and is billed on what its agent spends within it — tokens plus searches, page fetches, and PDFs; a deposit of `expected` is charged at kickoff, the rest as it runs up to the budget (unused deposit refunded; a failed build refunds everything; `typical` = recent p25–p75). A configured list build (`build_configured_dataset`) is its template's flat tier price. `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 tick (a fill of the dataset's rows), and every tick charges that again.\n\n**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.\n","datasets_vs_enrichment":"# List build vs enrichment\n\nA list build generates rows: `build_dataset` with `prompt` + `count` makes a new dataset and fills its input columns from the description (`input_columns` names them, else they are written from the prompt; `attributes` become its enrichment's output columns — with neither `input_columns`, `attributes`, nor `enrichment_id`, the output columns are written from the prompt too, one per fact it asks for, each with its prompt and type); `build_dataset_rows` adds more generated rows to a dataset you have, deduplicated on its `index_column`. The run's `output` is the rows it found; `get_dataset` and `list_dataset_rows` read them afterwards.\n\n**A list build finds rows. It does not research them.** It fills identifier columns only (e.g. Company + Website). `attributes` you pass are NOT filled in — they are saved as the output columns of the dataset's enrichment, which runs later as a separate paid step.\n\nTo get attributes filled, either:\n- pass `auto_fill: true` on `build_dataset` (`fill_webhook_url` is POSTed when the fill ends; the fill run appears under the dataset's `runs` as the rows land), or\n- after the build succeeds, call `enrich` with the `dataset_id`: alone (its enrichment holds the columns; `update_dataset` changes them) or with `prompt` + `attributes` (fills those columns, adding the ones the dataset lacks; `output` is refused there).\n\nA build result never contains attribute values. If a user asked for columns and you did not auto-fill, the next step is `enrich`, not another `build_dataset`.\n\nPut qualifiers in the `prompt` (\"Series A or later, based in the US\"). `count` is capped per account; `estimate_cost: true` shows charged now / typically / at most before anything is generated.\n","datasets":"# Datasets\n\nA 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.\n\n**Putting rows in a dataset and filling it is the preferred way to enrich.** `build_dataset` with `rows` + `attributes` (free), then `enrich` with the `dataset_id`: the rows and every filled cell are kept, one agent researches each row's columns together, a later fill runs only what is new or changed, and the user sees the grid in the Riveter app. `enrich` with inline `input` is for a one-off whose rows need not be kept.\n\n**An older account that has not completed its move to Datasets** answers `403 feature_not_enabled` from every tool below. Stop retrying and tell the user: open Datasets in the Riveter app (https://app.riveterhq.com/v2/datasets) and click \"Migrate to Datasets\"; the tools work once the copy completes.\n\n| tool | does |\n|---|---|\n| `build_dataset` | a new dataset with rows you send (`rows`, free) or rows Riveter generates (`prompt` + `count`, paid) |\n| `list_datasets` / `get_dataset` | the account's datasets; one dataset with its columns, `enrichment_id`, `row_count`, newest `runs` |\n| `add_dataset_rows` / `import_dataset_rows` | append rows as JSON objects or as CSV text (free) |\n| `build_dataset_rows` | more generated rows for an existing dataset (paid; needs a `index_column`) |\n| `list_dataset_rows` | rows in position order with every cell's `value` and `state` and the row's `credits`; page with `cursor`; `columns` + `compact: true` reads a whole column in one call |\n| `get_dataset_summary` | per-column counts without reading rows: cells by state, `filled`, `distinct_values`, the top `values` — \"how many of each\" |\n| `enrich` with `dataset_id` | fill cells (paid). Default scope: every `pending`, `stale`, or `failed` cell; narrow with `columns`, `rows`, `positions`, or `targets`; `states` picks the cell states to re-run, `force: true` is every state (Fill scope, below). `prompt` + `attributes` fills the named columns, adding the missing ones first (the `prompt` becomes the task only for a dataset that had no enrichment) |\n| `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` |\n| `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 |\n| `promote_dataset_column` | an output column becomes an input column; every row's value moves with it |\n| `delete_dataset_rows` / `delete_dataset` | remove rows by `ids` and / or `positions` (up to 100 per call) / the dataset |\n| `export_dataset` | the grid as CSV |\n\n## Output columns and the enrichment\n\nA 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\"?, \"tool\"? }`; `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`); `tool` pins a tool to the column: one call of it per row is the cell (below).\n\n`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, also when sent with column changes.\n\nAn output column may pin a tool (`\"tool\": \"linkedin_person_profile\"`): the row agent makes one call of it per row, with arguments it works out, and the result is the cell. `help` with `pinned_columns`.\n\n## Cell states\n\n`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).\n\n## Fill scope\n\nEvery pending, stale, or failed cell:\n```json\n{ \"dataset_id\": \"ds_018f6a70-1234-7abc-89ab-0123456789ab\" }\n```\nOne column on one row, even if filled:\n```json\n{ \"dataset_id\": \"ds_018f6a70-1234-7abc-89ab-0123456789ab\", \"columns\": [\"CEO\"], \"rows\": [\"row_018f6a70-aaaa-7abc-89ab-0123456789ab\"], \"force\": true }\n```\nRows 10–12 by position (from 1, as `list_dataset_rows` reports them):\n```json\n{ \"dataset_id\": \"ds_018f6a70-1234-7abc-89ab-0123456789ab\", \"positions\": [10, 11, 12] }\n```\nEvery `not_found` cell again, on rows 1–10 only (`states` picks the states to run; `force` is every state, so do not send both):\n```json\n{ \"dataset_id\": \"ds_018f6a70-1234-7abc-89ab-0123456789ab\", \"positions\": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], \"states\": [\"not_found\"] }\n```\nSeveral 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:\n```json\n{ \"dataset_id\": \"ds_018f6a70-1234-7abc-89ab-0123456789ab\", \"targets\": [{ \"columns\": [\"CEO\"], \"positions\": [3, 4] }, { \"columns\": [\"HQ\"], \"positions\": [20, 21, 22] }] }\n```\nA 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` naming the run and the positions), and a fill on every row is refused while any fill runs on the dataset.\n\n`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`.\n\nThe 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, `list_dataset_rows`; for what it filled by count, `get_dataset_summary`.\n\n## Generated rows\n\n`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`.\n\n## Index column\n\n`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).\n\n## Examples\n\nCreate with rows and two output columns, then `enrich` with the new `id`:\n```json\n{ \"name\": \"Targets\", \"input_columns\": [\"Company\", \"Website\"], \"rows\": [{ \"Company\": \"Apple\", \"Website\": \"apple.com\" }, { \"Company\": \"Shopify\" }], \"attributes\": [\"CEO\", \"HQ city\"] }\n```\nGenerate 50 rows and fill them when they land:\n```json\n{ \"name\": \"SF coffee\", \"prompt\": \"Specialty coffee roasters in San Francisco\", \"count\": 50, \"attributes\": [\"Founded year\", \"Flagship cafe address\"], \"auto_fill\": true }\n```\nThe same from the prompt alone — identity columns and typed output columns written from it:\n```json\n{ \"prompt\": \"Specialty coffee roasters in San Francisco, with the year each was founded and its flagship cafe's address\", \"count\": 50, \"auto_fill\": true }\n```\n`update_dataset`: change a column's prompt (its cells go `stale`), add one, remove one:\n```json\n{ \"output\": { \"CEO\": { \"prompt\": \"Full name of the current CEO\" }, \"Employee count\": { \"prompt\": \"Headcount as an integer\", \"output_schema\": { \"type\": \"integer\" } }, \"HQ city\": { \"delete\": true } } }\n```\n`update_dataset`: add an input column and make it the index column:\n```json\n{ \"input_columns\": { \"Ticker\": {} }, \"index_column\": \"Ticker\" }\n```\n`update_dataset_row`: fix an input value, set one cell, clear another:\n```json\n{ \"input\": { \"Website\": \"apple.com\" }, \"cells\": { \"CEO\": \"Tim Cook\", \"HQ city\": null } }\n```\n","pinned_columns":"# Pinned tool columns\n\nOn a dataset-engine enrichment an output column may pin a tool, by its `list_tools` name:\n```json\n{ \"name\": \"Profile\", \"prompt\": \"The LinkedIn profile of the row's person; find the canonical URL first when the row has none\", \"tool\": \"linkedin_person_profile\" }\n```\nThe row agent makes one call of that tool for the row and the call's result is the cell, as the tool returned it — no summary, no judgment on the value. What the agent decides is the call's arguments: it takes them from the row and the known values when they are there, otherwise it finds them first (a search for the person's canonical `https://linkedin.com/in/<slug>`, the company's domain from its site) and settles them from evidence before it calls. It is the legacy tool-only column on the dataset engine, without the fixed inputs: use it when the value is one tool's output (a LinkedIn profile, a page's text, a PDF's answer, a `code` transform of other columns) and the agent should be the one to work out what to run it on.\n\n**The prompt guides the call.** Say what to call the tool on and how to find it (\"the careers page of the company's site\", \"the LinkedIn profile of the current CEO, not the founder\"), and anything the tool should be told (a `code` function body, the question for a PDF). The agent never fills a pinned column from a search result, from another tool, or from memory; when the call fails or returns nothing it may call again with arguments corrected from evidence, and otherwise the cell is empty (`not_found`).\n\n**What the call takes and gives back.** `list_tools` says so per tool: `inputs` (each with `required`; `input_groups` when any one complete set is enough — the Hunter email finder takes a LinkedIn handle, or a domain and a name) and `returns`. Set the dataset up to carry the inputs — a LinkedIn URL column for the LinkedIn tools, a Website column for `scrape` — or make a researched column find them first; the agent reads the row before it researches.\n\n**Rules.** The pinned tool joins the enrichment's `tools`, so an opt-in tool must be enabled on the account and a `requires_api_key` tool must have a key linked (`list_tools` `enabled` / `api_key_linked`), else the write is refused. `output_schema` does not apply while a tool is pinned. The per-row budget follows the pinned tool's price: a column pinned to a 4-credit tool adds 1.5 + 4 credits per row when the budget is not locked (the research that settles the call's inputs costs budget too).\n\n**Edits.** `tool` on an `output` entry of `create_enrichment`, or on an `output` operation of `update_dataset` / `update_enrichment`; `\"tool\": null` unpins the column (back to a researched column). Pinning, unpinning, or changing the tool marks the column's filled cells `stale`.\n\n`update_dataset` with a researched URL column and two pinned columns:\n```json\n{ \"output\": { \"LinkedIn URL\": { \"prompt\": \"The person's canonical https://linkedin.com/in/<slug> URL\" }, \"Profile\": { \"prompt\": \"The profile at the row's LinkedIn URL\", \"tool\": \"linkedin_person_profile\" }, \"Homepage text\": { \"prompt\": \"The company's homepage, from the Website column\", \"tool\": \"scrape\" } } }\n```\nA `code` column transforming two other columns (the prompt carries the computation; the agent writes the code and passes the values):\n```json\n{ \"name\": \"Revenue per employee\", \"prompt\": \"Revenue divided by Employee count, rounded to a whole number; both from this row's columns\", \"tool\": \"code\" }\n```\n","output_schema":"# `output_schema`\n\nOn 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.\n\n**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` — gets the type of the one value it is about, from the task prompt, the column name, and the column prompt. The shapes, each one the app's Format picker reads back: email / URL / date / date-time (`{ \"type\": \"string\", \"format\": ... }`), `{ \"type\": \"integer\" }`, `{ \"type\": \"number\" }`, `{ \"type\": \"boolean\" }`, a count `{ \"type\": \"integer\", \"minimum\": 0 }`, a year `{ \"type\": \"integer\", \"minimum\": 1000, \"maximum\": 2100 }`, and the shapes JSON Schema has no `format` for, written as a fixed sentence on `description` plus a `pattern` where the value can be checked: a money amount (revenue, ARR, market cap, funding, price) as `{ \"type\": \"number\", \"description\": \"Amount in USD, as a plain number without symbols or abbreviations; convert when the source reports another currency\" }` (the currency the prompts name, else USD), a percentage (`\"A percentage as a plain number: 50 for 50%\"`), a phone number in E.164 (`\"pattern\": \"^\\\\+[1-9][0-9]{1,14}$\"`), a postal address on one line, a location (city, region, country), a country, a person's name, a company's name, a bare domain, a LinkedIn URL, a stock ticker. `get_enrichment` shows the exact schema a column got; send the same one yourself for such a column. The app shows a money or percentage cell with its unit (\"$2,900,000,000,000\", \"12.5%\"). Only prose stays untyped — a description, a summary, an industry, a job title — and a list or a structure. A caveat in a prompt does not change the type: \"ARR, mark unavailable when not reported\", \"support phone with source URL\", \"HQ address (note when remote)\" are money, phone, and address — a cell with no fitting value is `not_found` with the agent's reason, and the URLs it cited are on every cell's `sources`, so leave sources out of the prompt and keep one fact per column (\"ARR\" and \"ARR as-of date\" are two columns). The inferred schema is on the column in the response (`get_enrichment`); change or clear it with `update_dataset` / `update_enrichment`. Do not send `{ \"type\": \"string\" }` to sidestep a type you are unsure of — omit the schema and let the inference decide; it is for a column you want as free text no matter what, and the app shows it as Text. Changing an existing column's prompt never re-types it.\n\nWhat is accepted: `type` (`string`, `number`, `integer`, `boolean`, `array`, `object`), `enum`, `properties`, `required`, `items`, `description`, `minimum` / `maximum`, `maxItems`, `format`, `pattern` (a regular expression the string must match; the agent cannot answer a value outside it, so a cell with none is `not_found`). 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`.\n\nThe 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\").\n\nEach example is one entry of `output` on `create_enrichment`, or one operation of `output` on `update_dataset` / `update_enrichment`.\n\nA number:\n```json\n{ \"name\": \"Employee count\", \"prompt\": \"Current headcount\", \"output_schema\": { \"type\": \"integer\", \"minimum\": 0 } }\n```\nA choice:\n```json\n{ \"name\": \"Segment\", \"prompt\": \"Which segment fits the company best\", \"output_schema\": { \"type\": \"string\", \"enum\": [\"SMB\", \"Mid-market\", \"Enterprise\"] } }\n```\nAn object:\n```json\n{ \"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\"] } }\n```\nA list:\n```json\n{ \"name\": \"Competitors\", \"prompt\": \"Up to five direct competitors\", \"output_schema\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"maxItems\": 5 } }\n```\nFree text (no schema; a summary is never typed):\n```json\n{ \"name\": \"Summary\", \"prompt\": \"Two sentences on what the company sells and to whom\" }\n```\nFree text whatever the name suggests (an explicit schema is never replaced by an inferred one):\n```json\n{ \"name\": \"Email\", \"prompt\": \"The best address to write to, or how to reach them otherwise\", \"output_schema\": { \"type\": \"string\" } }\n```\n","limits":"# Limits\n\n| what | limit |\n|---|---|\n| `enrich` rows with `enrichment_id` | 10,000 per call |\n| `enrich` rows with `output` or `prompt` + `attributes` | 1,000 per call |\n| `attributes` (enrich and dataset endpoints) | 20 |\n| `build_dataset` / `build_dataset_rows` `count` | per-account cap; error message states it |\n| `get_run` / `get_run_result` `wait` | 0–50 seconds |\n| `list_*` `per_page` | 100 (`list_enrichments`: 50) |\n| agent tool calls per cell | `max_tool_calls`, default and max 10 |\n| JSON schema nesting | 10 levels |\n| rate limit | 30 requests / minute per endpoint group (`X-RateLimit-*` headers) |\n| dataset `rows` per `build_dataset` / `add_dataset_rows` call | 100 (more: `import_dataset_rows`) |\n| dataset input columns | 50, names up to 100 characters; a value up to 10,000 characters |\n| dataset output columns | 50 per enrichment; `prompt` up to 5,000 characters, `output_schema` up to 20,000 |\n| `delete_dataset_rows` `ids` + `positions` | 100 rows per call |\n| `list_dataset_rows` `limit` | 1,000 (default 100) |\n| `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` |\n\nAll list/array limits are also in each tool's schema as `maxItems` / `maximum`.\n","webhooks":"# Webhooks\n\nPass `webhook_url` on any run kickoff (a dataset's auto-fill: `fill_webhook_url`) and Riveter POSTs the finished run to it. Events: `run.completed`, `run.stopped`, `run.finished`. The endpoint must return 2xx. Transient failures (network errors, 5xx, 408, 425, 429) are retried up to 2 times; any other 4xx response is not retried.\n\nPayload (shared with the legacy API):\n```json\n{\n  \"event\": \"run.completed\",\n  \"run_key\": \"<uuid>\",\n  \"status\": \"success\",\n  \"enrichment_uuid\": \"<uuid>\", \"enrichment_name\": \"My Enrichment\",\n  \"credits_used\": 2.5,\n  \"completed_at\": \"2026-01-15T12:00:00Z\",\n  \"formatted_data\": { \"Company\": [{ \"value\": \"Apple\" }], \"Revenue\": [{ \"value\": \"383000000000\" }] },\n  \"status_details\": { \"...\": \"flat status fields\" },\n  \"run_status\": { \"run_key\": \"...\", \"status\": \"success\", \"...\": \"...\" }\n}\n```\n`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, with `dataset_id`). A monitor's tick adds `monitor_id`, delivers only when its `alert_rule` passes (`on_diff` = at least one cell changed), and with `output_format: include_diffs` each cell carries `previous_value` and `changed` (`help` → `monitors`). Extraction runs use `extraction_plan_run.*` events and add `extraction_plan_uuid`, `run_state`, `records_count`.\n\nUse 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.\n","monitors":"# Monitors: a dataset filled again on a schedule\n\nA monitor (`mon_...`) re-fills a dataset's output columns on a cadence and compares each cell with the previous tick. `create_monitor` takes `dataset_id` (or `enrichment_id` naming the dataset's enrichment), `cadence` (`daily` / `weekly` / `monthly`, with `hour`, `minute`, `timezone`, and `day_of_week` or `day_of_month`), and optionally `columns` — the output column names each tick fills (every one when omitted, including columns added later). A monitor takes no `input`: its rows are the dataset's, so add rows with `add_dataset_rows` and they are in the next tick.\n\n**Each tick** is an ordinary fill run (`run_...`, `type: enrichment`) on the dataset, forced so every targeted cell runs again, listed by `list_monitor_runs` and read by `get_run_result` as `{ \"rows\": [...] }`. Creation is free; every tick charges the fill price (rows × the enrichment's per-row budget at most). `estimate_cost: true` on `create_monitor` prices one tick.\n\n**Changes.** `alert_rule` decides when the webhook fires: `each_run`, `on_diff` (only when at least one monitored cell changed since the previous tick), or `disabled`. `output_format: include_diffs` adds `previous_value` and `changed` to every cell in the run's `output.rows` and the webhook payload; `current_only` sends the cells alone. A cell counts as changed when its value differs from the previous tick's (exact match; the app's diff rules can relax that per column or judge with a prompt). A row with no previous value counts as changed.\n\n`update_monitor` with `enabled: false` pauses, `enabled: true` resumes, `columns` changes what a tick fills, `webhook_url` re-points the delivery. A monitor on a legacy enrichment (fixed `input` rows; `legacy: true` in `list_monitors`) still runs; its responses carry `legacy` naming the dataset copy to recreate it on with `dataset_id`.\n","errors":"# Errors\n\nShape: `{ \"error\": { \"type\": \"<type>\", \"message\": \"<what to fix>\" } }`. The `message` names the field and the limit.\n\n| `type` | meaning | do |\n|---|---|---|\n| `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 |\n| `not_found` | unknown id | check the prefix (`run_`, `enr_`, `ds_`, `ext_`, `mon_`) and that it belongs to this account |\n| `forbidden` | key lacks access | the connection's API key cannot use this resource |\n| `duplicate_run_key` | `run_key` already used | pick a new `run_key` or fetch the existing run |\n| `insufficient_credits` | balance too low | `credit_detail` shows `credits_remaining`; reduce rows/columns or ask the user to top up |\n| `credit_cap_exceeded` | estimate `maximum` > your `max_credits` | raise `max_credits`, or shrink the request; `credit_detail` has the estimate |\n| `not_implemented` | unsupported combination | use the alternative named in `message` |\n| `dataset_in_use` / `enrichment_in_use` / `row_in_use` (409) | a fill is running on the dataset, enrichment, or row (`row_in_use`: an `input` write; `cells` land during a fill) | wait for it (`get_run`) or `stop_run`, then retry the edit |\n| `payload_too_large` (413) | body over 2 MB | split the rows across calls, or use `import_dataset_rows` |\n| `feature_not_enabled` (403) | an older account has not completed its move to Datasets | stop retrying; tell the user to open Datasets in the Riveter app (https://app.riveterhq.com/v2/datasets) and click \"Migrate to Datasets\", then retry once the copy completes. Every tool that names no dataset still works |\n\nAuthentication failures (`401`) use `{ \"request_status\": \"error\", \"message\": \"...\", \"error_type\": \"unauthorized\" }` — the MCP connection's key was revoked; the user must reconnect.\n\nA run that fails after kickoff reports `status: \"stopped\"` with an `error` on `get_run` — no error envelope, since the kickoff already returned 201.\n\nA `legacy` object in a response (`{ \"notice\", \"successor\", \"docs\" }`) means the call took a legacy mode — a legacy enrichment's id, a monitor on one, an `output` column with legacy-pipeline fields. It still worked; make the next call as `successor` says.\n"},"version":"3.1.1","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":"Fill in new columns for rows of data. The rows come from exactly one of `dataset_id` — a dataset Riveter keeps, the preferred way — or `input`, rows sent inline for a one-off run.\n\n## Preferred: fill a dataset (`dataset_id`)\n\nPut the rows in a dataset first — [POST /datasets](#tag/datasets/post/datasets) with `rows` (objects keyed by input column name) and the output columns to add, either `attributes` (their names; Riveter creates the dataset's enrichment from them) or the `enrichment_id` of a saved enrichment. That is free. Then fill it:\n\n```bash\ncurl -X POST \"https://api.riveterhq.com/v1/datasets\" \\\n  -H \"Authorization: Bearer YOUR_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"Q4 targets\",\n    \"rows\": [{ \"Company\": \"Apple\", \"Website\": \"apple.com\" }, { \"Company\": \"Shopify\", \"Website\": \"shopify.com\" }],\n    \"attributes\": [\"CEO\", \"Employee Count\", \"Industry\"]\n  }'\n\ncurl -X POST \"https://api.riveterhq.com/v1/enrich\" \\\n  -H \"Authorization: Bearer YOUR_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{ \"dataset_id\": \"ds_YOUR_DATASET_ID\" }'\n```\n\nThe dataset keeps the rows and every filled cell: one AI agent researches each row's columns together, a later call fills only what is new or changed (every `pending`, `stale`, or `failed` cell by default), rows are added, re-run, exported, and read back at any time ([GET /datasets/{id}/rows](#tag/datasets/get/datasets/{id}/rows)), and the grid is in the Riveter app.\n\nThe columns come from the dataset's enrichment, so no column config is sent (an `enrichment_id` is accepted only when it is that enrichment). `columns` (output column names), `rows` (row ids), and `positions` narrow the scope, and `force: true` re-runs cells that are already filled. `prompt` + `attributes` fills the named columns, adding the ones the dataset does not have yet (a dataset without an enrichment gets one from the `prompt`; an existing enrichment keeps its own task prompt, and the new columns are typed from their names). An `output` spec is a `400` — set a column's prompt, schema, or tool with [PATCH /datasets/{id}](#tag/datasets/patch/datasets/{id}) first.\n\n## Inline rows (`input`)\n\nFor a one-off run whose rows you do not need to keep. The rows come in the request and the results come back on the run only (`output`, columnar); the rows live on a single-run dataset that expires. The same engine fills them — one AI agent researches each row's columns together. `input` is a JSON object in columnar form:\n- Keys are column headers.\n- Values are arrays of strings. Every array must be the same length.\n- Each position across the arrays is one row.\n\nFor example, `{ \"Company\": [\"Apple\", \"Google\"], \"Website\": [\"apple.com\", \"google.com\"] }` is two rows:\n\n| Company | Website |\n|---------|-----------|\n| Apple   | apple.com |\n| Google  | google.com |\n\nWith `input`, tell Riveter what columns to add with exactly one of:\n\n1. A saved enrichment (`enrichment_id`) — fixed, tested config, the most consistent results.\n2. A prompt + attributes — describe the columns in natural language.\n3. A full output spec (`output`) — define each column exactly.\n\n### 1. run an existing enrichment (enrichment_id)\n\nPass the id of a saved enrichment (`enr_...`). First build and tune it — on a dataset in the [Riveter app](https://app.riveterhq.com/v2/datasets) (the dataset page's **API** button shows its `enrichment_id`) or with [POST /enrichments](#tag/enrich/post/enrichments) — then run it with new rows. The configuration is fixed and tested, so results are the most consistent of the three. Max 10,000 rows per request. Input column headers must match the enrichment's input columns. The id of a legacy enrichment (one built in the app's Enrichments page, before Datasets) still runs, on the legacy pipeline, and the response carries `legacy` naming its dataset copy to fill instead.\n\n```bash\ncurl -X POST \"https://api.riveterhq.com/v1/enrich\" \\\n  -H \"Authorization: Bearer YOUR_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"enrichment_id\": \"enr_YOUR_ENRICHMENT_ID\",\n    \"input\": {\"Company Name\": [\"Acme Corp\", \"Tech Solutions Inc\"]}\n  }'\n```\n\n### 2. run from a prompt + attributes\n\nProvide a natural-language `prompt` — the task the agent follows for every row — and an `attributes` array of output column names, each typed from its name. No setup. For repeatable results, put the rows in a dataset (above) or save the enrichment once and run it by `enrichment_id` (option 1). Max 1,000 rows per request, max 20 attributes.\n\n```bash\ncurl -X POST \"https://api.riveterhq.com/v1/enrich\" \\\n  -H \"Authorization: Bearer YOUR_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"prompt\": \"Research each company\",\n    \"attributes\": [\"CEO\", \"Employee Count\", \"Industry\"],\n    \"input\": {\"Company\": [\"Apple\", \"Google\"]},\n    \"webhook_url\": \"https://your-server.com/webhook\"\n  }'\n```\n\n### 3. run from a full output spec\n\nDefine each output column: its `prompt`, an optional `output_schema` (a JSON Schema the value must match; inferred when omitted and the value is one plain value), and an optional `tool` to pin (one call of that tool per row is the cell). `contexts` is accepted and ignored — the agent reads the whole row. Max 1,000 rows per request. See the `EnrichmentOutputSpec` schema.\n\n```bash\ncurl -X POST \"https://api.riveterhq.com/v1/enrich\" \\\n  -H \"Authorization: Bearer YOUR_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"output\": {\n      \"Employee Count\": {\n        \"prompt\": \"Find the number of employees at this company\",\n        \"output_schema\": { \"type\": \"integer\" }\n      }\n    },\n    \"input\": {\"Company\": [\"Apple\", \"Google\"]}\n  }'\n```\n\nA column that uses a legacy-pipeline field — `format` / `format_details`, `run_when` / `run_when_config`, `tools`, `max_tool_calls`, or a `tool` with its parameters (`\"url\": \"Website\"`) — keeps the whole run on the legacy pipeline (one agent per cell, a legacy enrichment), and the response carries `legacy` naming the fields. Those fields are documented in the [legacy API docs](./openapi.legacy.yaml).\n\n## Scoping a dataset fill\n\nOne column on every row that needs it:\n\n```bash\ncurl -X POST \"https://api.riveterhq.com/v1/enrich\" \\\n  -H \"Authorization: Bearer YOUR_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"dataset_id\": \"ds_YOUR_DATASET_ID\",\n    \"columns\": [\"CEO\"]\n  }'\n```\n\n`rows` (row ids) and `positions` (counted from 1) pick rows, `targets` is a list of such `{ columns, rows, positions }` boxes in one run, `states` names the cell states to re-run (`not_found`, `ok`, ...), and `force: true` re-runs every cell in scope. 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.\n\n## After the kickoff\n\nThe 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. A dataset fill's `output` is `{ \"rows\": [...] }` — the rows it ran, each with its `cells` by column name; an inline run's `output` is columnar, one entry per input row.\n","operationId":"enrich","x-mcp-open-world":true,"x-mcp-description":"Add AI-researched columns to rows. Rows come from exactly one of `dataset_id` (`ds_...`, preferred) or `input` (inline, columnar: `{ \"Header\": [\"row1\", \"row2\"] }`, all arrays equal length; a one-off, nothing kept).\n\nPreferred: put the rows in a dataset first (`build_dataset` with `rows` + `attributes` or `enrichment_id`, free; it keeps the rows and filled cells), then `enrich` with its `dataset_id`: fills its cells with its enrichment; no column config needed. Default scope: every `pending`, `stale`, or `failed` cell; `columns`, `rows`, `positions` narrow it; `force: true` re-runs filled cells. `prompt` + `attributes` fills the named columns, adding the missing ones first; `output` is refused (`update_dataset` sets a column's prompt / schema / tool). `help` with `datasets`.\n\nWith `input`, column config is exactly one of, most consistent first:\n1. `enrichment_id` — a saved enrichment (`enr_...`, `list_enrichments`): fixed, tested config; up to 10,000 rows; input headers must match its input columns (`get_enrichment`).\n2. `prompt` + `attributes` — the task and up to 20 column names; no setup; up to 1,000 rows.\n3. `output` — per column `prompt`, optional `output_schema`, optional pinned `tool`; up to 1,000 rows. `help` with `column_config` or `examples` first.\n\nReturns 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` prices it without running. A `legacy` key in the response means the call took a legacy mode (a legacy enrichment's id, legacy-pipeline column fields); follow `legacy.successor`.\n","x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst run = await riveter.enrich({\n  enrichment_id: \"enr_YOUR_ENRICHMENT_ID\",\n  input: { \"Company Name\": [\"Acme Corp\", \"Tech Solutions Inc\"] },\n});\nconst result = await riveter.runs.waitForResult(run.id);\nconsole.log(result.output);\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\nrun = riveter.enrich(\n    enrichment_id=\"enr_YOUR_ENRICHMENT_ID\",\n    input={\"Company Name\": [\"Acme Corp\", \"Tech Solutions Inc\"]},\n)\nresult = riveter.runs.wait_for_result(run.id)\nprint(result.output)\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\nrun = riveter.enrich(\n  enrichment_id: \"enr_YOUR_ENRICHMENT_ID\",\n  input: { \"Company Name\" => [\"Acme Corp\", \"Tech Solutions Inc\"] }\n)\nresult = riveter.runs.wait_for_result(run.id)\nputs result.output\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\nctx := context.Background()\nrun, err := client.Enrich(ctx, riveter.EnrichParams{\n    EnrichmentID: \"enr_YOUR_ENRICHMENT_ID\",\n    Input:        map[string][]string{\"Company Name\": {\"Acme Corp\", \"Tech Solutions Inc\"}},\n})\nresult, err := client.Runs.WaitForResult(ctx, run.ID, nil)\nfmt.Println(string(result.Output))\n"}],"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: a spec per output column (prompt, output_schema, tool)"},"prompt":{"type":"string","description":"Config source: the task the agent follows for every row (requires attributes)"},"attributes":{"type":"array","items":{"type":"string"},"maxItems":20,"description":"Config source: the output column names, typed from their names (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. Fills the dataset's cells with its bound enrichment; no column config with it."},"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>\"),\nso it is limited to letters, digits, and . _ ~ - characters. A duplicate returns 409.\n"},"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 (a task and column names)","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":"A spec per output column","value":{"output":{"Employee Count":{"prompt":"Find the number of employees at this company","output_schema":{"type":"integer"}}},"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 rows 10–12, even if filled","value":{"dataset_id":"ds_018f6a70-1234-7abc-89ab-0123456789ab","columns":["CEO"],"positions":[10,11,12],"force":true}}}}}},"responses":{"200":{"description":"estimate_cost only — the credit estimate; nothing was started","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EstimateCostResult"}}}},"201":{"description":"Run started — poll /runs/{id} or wait for the webhook","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Run"}}}},"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.\n\n`?wait=N` (max 50) long-polls: the request holds until the run finishes or the budget elapses.\n","operationId":"getRun","x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst run = await riveter.runs.get(\"run_YOUR_RUN_ID\");\nconsole.log(run.status, run.progress);\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\nrun = riveter.runs.get(\"run_YOUR_RUN_ID\")\nprint(run.status, run.progress)\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\nrun = riveter.runs.get(\"run_YOUR_RUN_ID\")\nputs run.status\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\nrun, err := client.Runs.Get(context.Background(), \"run_YOUR_RUN_ID\")\nfmt.Println(run.Status)\n"}],"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`, `null` until the run finishes.\n\nPass `?wait=N` (max 50) to long-poll: the request holds until the run finishes or the budget elapses, whichever comes first.\n\n## Output shape by run type\n- **enrichment** — an object mapping column headers to arrays of cell objects: `{ \"Company\": [{\"value\": \"Apple\"}], \"CEO\": [{\"value\": \"Tim Cook\"}] }`\n- **dataset_build** — the same columnar shape as enrichment\n- **extraction** — the extracted records as an array of JSON objects (matching your `output_record_json_schema`)\n- **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.)\n- **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.\n- **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`). `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.\n- **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.\n","operationId":"getRunResult","x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\n// One 30s long-poll:\nconst result = await riveter.runs.result(\"run_YOUR_RUN_ID\", { wait: 30 });\n// Or keep polling until the run finishes (default budget 10 min):\nconst finished = await riveter.runs.waitForResult(\"run_YOUR_RUN_ID\");\nconsole.log(finished.output);\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\n# One 30s long-poll:\nresult = riveter.runs.result(\"run_YOUR_RUN_ID\", wait=30)\n# Or keep polling until the run finishes (default budget 10 min):\nfinished = riveter.runs.wait_for_result(\"run_YOUR_RUN_ID\")\nprint(finished.output)\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\n# One 30s long-poll:\nresult = riveter.runs.result(\"run_YOUR_RUN_ID\", wait: 30)\n# Or keep polling until the run finishes (default budget 10 min):\nfinished = riveter.runs.wait_for_result(\"run_YOUR_RUN_ID\")\nputs finished.output\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\nctx := context.Background()\n// One 30s long-poll:\nresult, err := client.Runs.Result(ctx, \"run_YOUR_RUN_ID\", &riveter.ResultOptions{Wait: 30})\n// Or keep polling until the run finishes (default budget 10 min):\nfinished, err := client.Runs.WaitForResult(ctx, \"run_YOUR_RUN_ID\", nil)\nfmt.Println(string(finished.Output))\n"}],"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.\n","operationId":"stopRun","x-mcp-destructive":true,"x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst run = await riveter.runs.stop(\"run_YOUR_RUN_ID\");\nconsole.log(run.status); // \"stopped\"\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\nrun = riveter.runs.stop(\"run_YOUR_RUN_ID\")\nprint(run.status)  # \"stopped\"\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\nrun = riveter.runs.stop(\"run_YOUR_RUN_ID\")\nputs run.status # \"stopped\"\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\nrun, err := client.Runs.Stop(context.Background(), \"run_YOUR_RUN_ID\")\nfmt.Println(run.Status) // \"stopped\"\n"}],"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.\n\nFilter by `type` (comma-separated), `status`, `enrichment_id`, `dataset_id`, `monitor_id`, and `created_after` / `created_before` (ISO 8601). Paginate with `page` / `per_page`. `enrichment_id` naming a legacy enrichment still filters and the response carries `legacy`.\n\nPass `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.\n","operationId":"listRuns","x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst page = await riveter.runs.list({ status: \"success\" });\nfor await (const run of page) { // auto-pages through every result\n  console.log(run.id, run.type);\n}\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\npage = riveter.runs.list(status=\"success\")\nfor run in page.auto_paging_iter():  # pages through every result\n    print(run.id, run.type)\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\npage = riveter.runs.list(status: \"success\")\npage.auto_paging_each do |run| # pages through every result\n  puts \"#{run.id} #{run.type}\"\nend\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\nctx := context.Background()\npage, err := client.Runs.List(ctx, &riveter.ListRunsParams{Status: \"success\"})\nfor {\n    for _, run := range page.Runs {\n        fmt.Println(run.ID, run.Type)\n    }\n    if !page.HasNextPage() {\n        break\n    }\n    page, err = page.NextPage(ctx)\n}\n"}],"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":"dataset_id","in":"query","required":false,"description":"Only runs on this dataset (ds_...) — its fills and its list generations","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=...`.\n","operationId":"runsSummary","x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst summary = await riveter.runs.summary();\nconsole.log(summary.counts);\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\nsummary = riveter.runs.summary()\nprint(summary.counts)\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\nsummary = riveter.runs.summary\nputs summary.counts\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\nsummary, err := client.Runs.Summary(context.Background())\nfmt.Printf(\"%+v\\n\", summary.Counts)\n"}],"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`, a pinned `tool`), `tools`, `budget_credits_per_row`, and `version` (goes up on every column change; a dataset cell filled under an older version reads `stale`).\n\nThe id of a legacy enrichment (one built in the app's Enrichments page, before Datasets) answers its older shape, documented in the [legacy API docs](./openapi.legacy.yaml), with `legacy` naming its dataset copy ([GET /datasets/{id}](#tag/datasets/get/datasets/{id})).\n","operationId":"getEnrichment","x-mcp-description":"One enrichment by id: `prompt`, `input_columns` (the headers rows must carry), `output` columns (`name`, `prompt`, `output_schema`, a pinned `tool` when the column is one tool call), `tools`, `budget_credits_per_row`, `version`. A `legacy` key means the id is a legacy enrichment's; its `successor` names the dataset copy.\n","x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst enrichment = await riveter.enrichments.get(\"enr_YOUR_ENRICHMENT_ID\");\nconsole.log(enrichment.input, enrichment.output);\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\nenrichment = riveter.enrichments.get(\"enr_YOUR_ENRICHMENT_ID\")\nprint(enrichment.input, enrichment.output)\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\nenrichment = riveter.enrichments.get(\"enr_YOUR_ENRICHMENT_ID\")\nputs enrichment.input.inspect\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\nenrichment, err := client.Enrichments.Get(context.Background(), \"enr_YOUR_ENRICHMENT_ID\")\nfmt.Println(enrichment.Input)\n"}],"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; `{ \"tool\": \"linkedin_person_profile\" }` pins a tool to it — one call of it per row is the cell, the agent choosing the call's arguments (the [ColumnToolPin](#model/columntoolpin); `\"tool\": null` unpins); `{ \"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, schema, or pin read `stale` on datasets bound to this enrichment and are re-run by their next fill. A rename changes nothing on the cells.\n\nThe 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.\n\nRefused 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/patch/datasets/{id}), which copies a shared enrichment first.\n\nThe id of a legacy enrichment keeps its older contract (the [legacy API docs](./openapi.legacy.yaml)) and answers with `legacy` naming its dataset copy.\n","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, `{ \"tool\": \"<list_tools name>\" }` pins a tool to it — one call per row is the cell, the agent choosing the arguments (`null` unpins; `help` with `pinned_columns`), `{ \"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`.\n","x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst enrichment = await riveter.enrichments.update(\"enr_YOUR_ENRICHMENT_ID\", {\n  output: {\n    CEO: { prompt: \"Full name of the current CEO\" },\n    \"Employee count\": { prompt: \"Current headcount\", output_schema: { type: \"integer\", minimum: 0 } },\n  },\n});\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\nenrichment = riveter.enrichments.update(\n    \"enr_YOUR_ENRICHMENT_ID\",\n    output={\n        \"CEO\": {\"prompt\": \"Full name of the current CEO\"},\n        \"Employee count\": {\"prompt\": \"Current headcount\", \"output_schema\": {\"type\": \"integer\", \"minimum\": 0}},\n    },\n)\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\nenrichment = riveter.enrichments.update(\n  \"enr_YOUR_ENRICHMENT_ID\",\n  output: {\n    \"CEO\" => { prompt: \"Full name of the current CEO\" },\n    \"Employee count\" => { prompt: \"Current headcount\", output_schema: { type: \"integer\", minimum: 0 } }\n  }\n)\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\nenrichment, err := client.Enrichments.Update(context.Background(), \"enr_YOUR_ENRICHMENT_ID\",\n    riveter.UpdateEnrichmentParams{\n        Output: map[string]riveter.OutputColumnOperation{\n            \"CEO\":            {Prompt: \"Full name of the current CEO\"},\n            \"Employee count\": {Prompt: \"Current headcount\", OutputSchema: map[string]any{\"type\": \"integer\", \"minimum\": 0}},\n        },\n    })\n"}],"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_contact; custom_apollo_people_search once enabled on the account 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. A legacy enrichment's id answers `422 unsupported` with `legacy`; those are deleted in the app.\n","operationId":"deleteEnrichment","x-mcp-destructive":true,"x-mcp-description":"Delete an enrichment by id. Runs in flight finish; a dataset bound to it keeps its columns. A legacy enrichment's id is refused (422 with `legacy`); those are deleted in the app.\n","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":{"post":{"summary":"create enrichment","description":"Create an enrichment without running it: 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/get/tools)). Each `output` entry is a column name or `{ \"name\", \"prompt\"?, \"output_schema\"?, \"tool\"? }`; `output_schema` is a JSON Schema for the cell value (see `help` → `output_schema`); without one, the column is typed from the prompts as the one value it is about (email, URL, date, year, count, money, percentage, phone, address, a person's or company's name, ...; only prose stays free text); `tool` pins a tool to the column — `\"tool\": \"linkedin_person_profile\"`, the [ColumnToolPin](#model/columntoolpin): the agent makes one call of it per row, with arguments it works out from the row and its research, and the call's result is the cell. Output names may not repeat an input column.\n\n**From a prompt alone.** Leave `output` out and the recipe is written from the `prompt`: one output column per fact it asks to find about each row, each with its own prompt and type, plus the `name` and `input_columns` when you give none (\"Research each company: its CEO, employee count, latest funding round date and amount, and whether it is hiring\" → `CEO`, `Employee count`, `Latest funding round date`, `Latest funding round amount`, `Hiring`, typed). The response shows what was written; adjust it with [PATCH /enrichments/{id}](#tag/enrich/patch/enrichments/{id}). A prompt that names no facts to find is refused (`422`): add `output`.\n\nThe enrichment comes with the **dataset it fills**: an empty dataset bound to it, named after it, whose input columns are `input_columns` (one `Input` column when you give none; the first build names it, or rename it with [PATCH /datasets/{id}](#tag/datasets/patch/datasets/{id})). It is on the Datasets page of the app at once. The response is the [enrichment](#model/enrichment) plus `dataset_id` (its id) and `app_url` (the dataset in the app).\n\nThen add rows to the dataset — [POST /datasets/{id}/rows](#tag/datasets/post/datasets/{id}/rows), [rows/import](#tag/datasets/post/datasets/{id}/rows/import) for a CSV, or [rows/build](#tag/datasets/post/datasets/{id}/rows/build) to generate them — and fill it with [POST /enrich](#tag/enrich/post/enrich) (`dataset_id`): the rows and cells are kept and the dataset shows them. Or run the enrichment once on rows you send inline (`enrichment_id` + `input`), nothing kept. Editing the enrichment through its dataset (the app's Column tab, [PATCH /datasets/{id}](#tag/datasets/patch/datasets/{id})) rebinds the dataset to a private copy; [PATCH /enrichments/{id}](#tag/enrich/patch/enrichments/{id}) edits this enrichment itself.\n\n`{ \"dataset_id\": \"ds_...\" }` alone answers the dataset's own enrichment as `{ id, name, app_url, dataset_id }` (nothing is created; `422` while the dataset has no output columns — [PATCH /datasets/{id}](#tag/datasets/patch/datasets/{id}) adds them). The same body with a legacy list build's id, documented in the [legacy API docs](./openapi.legacy.yaml), creates a legacy enrichment and answers with `legacy`.\n","operationId":"createEnrichment","x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst created = await riveter.enrichments.create({\n  name: \"Company basics\",\n  prompt: \"Research each company from its name and website.\",\n  input_columns: [\"Company\", \"Website\"],\n  output: [\"CEO\", { name: \"Employee count\", output_schema: { type: \"integer\", minimum: 0 } }],\n});\nconsole.log(created.id, created.dataset_id); // enr_..., ds_...\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\ncreated = riveter.enrichments.create(\n    name=\"Company basics\",\n    prompt=\"Research each company from its name and website.\",\n    input_columns=[\"Company\", \"Website\"],\n    output=[\"CEO\", {\"name\": \"Employee count\", \"output_schema\": {\"type\": \"integer\", \"minimum\": 0}}],\n)\nprint(created.id, created.dataset_id)  # enr_..., ds_...\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\ncreated = riveter.enrichments.create(\n  name: \"Company basics\",\n  prompt: \"Research each company from its name and website.\",\n  input_columns: [\"Company\", \"Website\"],\n  output: [\"CEO\", { name: \"Employee count\", output_schema: { type: \"integer\", minimum: 0 } }]\n)\nputs created.id, created.dataset_id # enr_..., ds_...\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\ncreated, err := client.Enrichments.Create(context.Background(), riveter.CreateEnrichmentParams{\n    Name:         \"Company basics\",\n    Prompt:       \"Research each company from its name and website.\",\n    InputColumns: []string{\"Company\", \"Website\"},\n    Output:       []riveter.OutputColumnSpec{{Name: \"CEO\"}, {Name: \"Employee count\", OutputSchema: map[string]any{\"type\": \"integer\", \"minimum\": 0}}},\n})\nfmt.Println(created.ID, created.DatasetID) // enr_..., ds_...\n"}],"x-mcp-description":"Create an enrichment (no run, free) and the empty dataset it fills, bound to it and in the app at `app_url`. A recipe: `prompt` (the task), `output` columns (names, or `{ name, prompt, output_schema, tool }` — `tool: \"<list_tools name>\"` pins a tool: one call per row, arguments chosen by the agent, result is the cell; `help` with `output_schema` / `pinned_columns`), 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`). Prefer `prompt` alone: the output columns (one typed column per fact asked for), name, and input columns are written from it; the response shows them. Then `add_dataset_rows` to the `dataset_id` and `enrich` with it (preferred), or `enrich` with `enrichment_id` + `input` for a one-off. `dataset_id` alone answers a dataset's own enrichment as `{ id, name, app_url, dataset_id }` (422 without output columns; add them with `update_dataset`).\n","tags":["Enrich"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"dataset_id":{"type":"string","description":"Alone: a dataset's id (ds_...); answers its own enrichment. Not with the fields below."},"name":{"type":"string","maxLength":200,"description":"Recipe: display name"},"prompt":{"type":"string","maxLength":50000,"description":"Recipe: 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: the headers rows must carry, the dataset's input columns. Omit for any (one `Input` column), or, without output, for the ones the prompt implies."},"output":{"type":"array","items":{"$ref":"#/components/schemas/OutputColumnSpec"},"maxItems":50,"description":"Recipe: the columns to fill; a plain string is a name whose type is inferred. Omit it and the columns are written from the prompt, one per fact it asks for, each typed."},"budget":{"type":"number","minimum":0.1,"maximum":100,"description":"Recipe: credits the row agent may spend per row. Default 1.5 per output column."},"budget_locked":{"type":"boolean","default":false,"description":"Recipe: 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: exactly these tools (GET /tools). Replaces the default set."},"additional_tools":{"type":"array","items":{"type":"string"},"description":"Recipe: the default tools plus these (GET /tools; some need enabling on the account or a linked key)."}}},"examples":{"company_research":{"summary":"Save a recipe — 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}},"from_prompt_alone":{"summary":"The recipe written from the prompt — columns, name, and input columns","value":{"prompt":"Research each company from its name and website: the CEO, employee count, latest funding round date and amount, and whether it is hiring."}},"datasets_own_enrichment":{"summary":"A dataset's own enrichment","value":{"dataset_id":"ds_018f6a70-1234-7abc-89ab-0123456789ab"}}}}}},"responses":{"201":{"description":"Save a recipe — the saved enrichment plus `dataset_id` and `app_url`, the dataset it fills. With `dataset_id` alone — the dataset's own enrichment as `{ id, name, app_url, dataset_id }`","content":{"application/json":{"schema":{"oneOf":[{"title":"Save a recipe","allOf":[{"$ref":"#/components/schemas/Enrichment"},{"type":"object","properties":{"dataset_id":{"type":"string","description":"The dataset created for the enrichment (ds_...); add rows to it and fill it"},"app_url":{"type":"string","format":"uri","description":"The dataset in the app"}}}]},{"title":"A dataset's own enrichment","type":"object","properties":{"id":{"type":"string","description":"The dataset's enrichment (enr_...)"},"name":{"type":"string"},"app_url":{"type":"string","format":"uri","description":"The dataset in the app"},"dataset_id":{"type":"string"},"legacy":{"$ref":"#/components/schemas/LegacyNotice"}}}]}}}},"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 recipes saved with [POST /enrichments](#tag/enrich/post/enrichments) (`prompt` + `output`) and bound to datasets, and the account's legacy enrichments (built in the app's Enrichments page, before Datasets). 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 is on [GET /enrichments/{id}](#tag/enrich/get/enrichments/{id}).\n\nA legacy enrichment's item carries `status`, `app_url`, and — once the account's copy into Datasets has run — `dataset_id`, its dataset copy: fill that with [POST /enrich](#tag/enrich/post/enrich) instead of the legacy id. A saved recipe has none of the three. 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.\n","operationId":"listEnrichments","x-mcp-description":"The account's enrichments, newest first: id, name, input / output column names. A legacy enrichment's item also has `status`, `app_url`, and `dataset_id` (its dataset copy: `enrich` that instead). Paginate with `page` / `per_page` (max 50); filter by `status` (legacy only), `name`. `get_enrichment` has the columns' config.\n","x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst page = await riveter.enrichments.list({ name: \"contact\" });\nfor await (const enrichment of page) {\n  console.log(enrichment.id, enrichment.name, enrichment.input_columns);\n}\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\npage = riveter.enrichments.list(name=\"contact\")\nfor enrichment in page.auto_paging_iter():\n    print(enrichment.id, enrichment.name, enrichment.input_columns)\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\nriveter.enrichments.list(name: \"contact\").auto_paging_each do |enrichment|\n  puts \"#{enrichment.id} #{enrichment.name} #{enrichment.input_columns}\"\nend\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\npage, err := client.Enrichments.List(context.Background(), &riveter.ListEnrichmentsParams{Name: \"contact\"})\nfor _, enrichment := range page.Enrichments {\n    fmt.Println(enrichment.ID, enrichment.Name, enrichment.InputColumns)\n}\n"}],"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":"Legacy enrichments only"},"app_url":{"type":"string","format":"uri","description":"Legacy enrichments only"},"dataset_id":{"type":"string","description":"Legacy enrichments only — the dataset copy (ds_...) to fill instead, once the account's copy into Datasets has run"},"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","dataset_id":"ds_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5c","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"}}}},"/datasets":{"post":{"summary":"build dataset","description":"Create a dataset: from rows you have, or from a prompt Riveter finds rows for.\n\n## What a dataset is\n\nA 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.\n\nRows are deduplicated on the first input column (`index_column`; change it with [PATCH /datasets/{id}](#tag/datasets/patch/datasets/{id})). Every row has an id (`row_...`) and a position counted from 1.\n\n**Not on Datasets yet?** New accounts are. An older account moves itself: open [Datasets](https://app.riveterhq.com/v2/datasets) in the app and click **Migrate to Datasets** — its enrichments and list builds are copied into datasets, and every endpoint in this section opens when the copy completes. Until then they answer `403` with `type: \"feature_not_enabled\"`.\n\n## 1. Build a dataset from rows you have\n\nThis is the preferred way to enrich data you already hold. The request is the rows plus the columns to add: the `enrichment_id` of an enrichment you already have — from a dataset in the app (the dataset page's **API** button shows it), from [GET /datasets/{id}](#tag/datasets/get/datasets/{id}), or from [GET /enrichments](#tag/enrich/get/enrichments); or save a new one with [POST /enrichments](#tag/enrich/post/enrichments) — or, with no enrichment yet, `attributes`, the output column names: Riveter creates the dataset's enrichment from them (its columns typed from their names; set a column's prompt, format, or tool with [PATCH /datasets/{id}](#tag/datasets/patch/datasets/{id})). Rows are objects keyed by input column name (an enrichment's input columns, [GET /enrichments/{id}](#tag/enrich/get/enrichments/{id}) lists them; with `attributes`, `input_columns` or the keys of the rows), up to 100 per request; add more with [POST /datasets/{id}/rows](#tag/datasets/post/datasets/{id}/rows) or a CSV on [POST /datasets/{id}/rows/import](#tag/datasets/post/datasets/{id}/rows/import).\n\n```bash\ncurl -X POST \"https://api.riveterhq.com/v1/datasets\" \\\n  -H \"Authorization: Bearer YOUR_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"Q4 targets\",\n    \"enrichment_id\": \"enr_YOUR_ENRICHMENT_ID\",\n    \"rows\": [\n      { \"Company\": \"Apple\", \"Website\": \"apple.com\" },\n      { \"Company\": \"Shopify\", \"Website\": \"shopify.com\" }\n    ]\n  }'\n```\n\nThe response is the dataset: `id` (`ds_...`), `input_columns`, `enrichment_id`, `output_columns`, `row_count`. Nothing has run yet. Fill the cells:\n\n```bash\ncurl -X POST \"https://api.riveterhq.com/v1/enrich\" \\\n  -H \"Authorization: Bearer YOUR_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{ \"dataset_id\": \"ds_YOUR_DATASET_ID\" }'\n```\n\nThis 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:\n\n```bash\ncurl \"https://api.riveterhq.com/v1/runs/RUN_ID/result?wait=50\" \\\n  -H \"Authorization: Bearer YOUR_API_KEY\"\n```\n\n`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/get/datasets/{id}/rows) is the whole grid at any time, filled cells included.\n\n## 2. Large datasets\n\n`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).\n\n```bash\ncurl -o rows.jsonl \"$(curl -s \"https://api.riveterhq.com/v1/runs/RUN_ID/result\" \\\n  -H \"Authorization: Bearer YOUR_API_KEY\" | jq -r '.output_files.jsonl.url')\"\n```\n\nTwo more ways to read a large grid: page [GET /datasets/{id}/rows](#tag/datasets/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/get/datasets/{id}/export) (`output_format` `jsonl`, `csv`, or `xlsx`).\n\n## 3. Build a dataset from a prompt\n\nNo 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.\n\nOr send `prompt` and `count` alone and the whole dataset is set up from the prompt: the identity columns, and one output column per fact the prompt asks to find about each row, each with its own prompt and type (\"Top 50 US fintech startups with their CEO, total funding, and founding year\" → rows under `Company`, columns `CEO`, `Total funding`, `Founding year`). A prompt about rows alone gets no output columns. Name `input_columns` yourself and the output columns come from `attributes` or `enrichment_id` only, as above.\n\n```bash\ncurl -X POST \"https://api.riveterhq.com/v1/datasets\" \\\n  -H \"Authorization: Bearer YOUR_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"SF coffee roasters\",\n    \"prompt\": \"Specialty coffee roasters in San Francisco\",\n    \"count\": 50,\n    \"input_columns\": [\"Roaster\", \"Website\"],\n    \"attributes\": [\"Founded year\", \"Number of cafes\"],\n    \"auto_fill\": true\n  }'\n```\n\nThe 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/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/post/datasets/{id}/rows/build).\n\nThe list builder's older body on this path (`max_items`, `identifiers`, `qualifiers`, ...) still runs and is documented in [the legacy API docs](?spec=legacy#tag/retired-resource-endpoints/post/datasets); its response carries `legacy` with the field mapping. New code sends `prompt` + `count`.\n","operationId":"buildDataset","x-mcp-open-world":true,"x-mcp-description":"Build a dataset: rows you send or rows Riveter finds. `rows` (≤ 100 objects keyed by input column name, free) or `prompt` + `count` (generated, paid); output columns from `attributes` or `enrichment_id`, or, with `prompt` + `count` alone, written from the prompt (one typed column per fact it asks for) along with the identity columns; `auto_fill: true` fills generated rows when they land; `budget` caps a generation's spend (default 50 + 1.5 per row; deposit of expected charged now, rest as it runs). Returns the dataset (`id`, `ds_...`) and `run` (the generation run, or null); then `enrich` with `dataset_id` alone. `estimate_cost: true` prices it first. `help` with `datasets`.\n","x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst dataset = await riveter.datasets.create({\n  name: \"US fintech startups\",\n  prompt: \"Top 100 US fintech startups\",\n  count: 100,\n  input_columns: [\"Company\", \"Website\"],\n  attributes: [\"CEO\", \"Funding raised\"],\n  auto_fill: true,\n});\nconst result = await riveter.runs.waitForResult(dataset.run!.id);\nconsole.log(result.status, dataset.id);\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\ndataset = riveter.datasets.create(\n    name=\"US fintech startups\",\n    prompt=\"Top 100 US fintech startups\",\n    count=100,\n    input_columns=[\"Company\", \"Website\"],\n    attributes=[\"CEO\", \"Funding raised\"],\n    auto_fill=True,\n)\nresult = riveter.runs.wait_for_result(dataset.run.id)\nprint(result.status, dataset.id)\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\ndataset = riveter.datasets.create(\n  name: \"US fintech startups\",\n  prompt: \"Top 100 US fintech startups\",\n  count: 100,\n  input_columns: [\"Company\", \"Website\"],\n  attributes: [\"CEO\", \"Funding raised\"],\n  auto_fill: true\n)\nresult = riveter.runs.wait_for_result(dataset.run.id)\nputs \"#{result.status} #{dataset.id}\"\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\nctx := context.Background()\ndataset, err := client.Datasets.Create(ctx, riveter.CreateDatasetParams{\n    Name:         \"US fintech startups\",\n    Prompt:       \"Top 100 US fintech startups\",\n    Count:        100,\n    InputColumns: []string{\"Company\", \"Website\"},\n    Attributes:   []string{\"CEO\", \"Funding raised\"},\n    AutoFill:     true,\n})\nresult, err := client.Runs.WaitForResult(ctx, dataset.Run.ID, nil)\nfmt.Println(result.Status, dataset.ID)\n"}],"tags":["Datasets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":200,"description":"Display name"},"input_columns":{"type":"array","items":{"type":"string"},"maxItems":50,"description":"Input column names (row identity). Optional with rows; with prompt, the identity columns to generate — omitted, they are written from the prompt, and with no attributes / enrichment_id the output columns are too."},"rows":{"type":"array","items":{"type":"object","additionalProperties":{"type":"string"}},"maxItems":100,"description":"The rows, keyed by input column name. Up to 100; more with POST /datasets/{id}/rows/import."},"prompt":{"type":"string","description":"What the rows are, for Riveter to find them. Requires count."},"count":{"type":"integer","minimum":1,"maximum":12000,"description":"How many rows to generate for prompt. Capped per account."},"budget":{"$ref":"#/components/schemas/ListBuildBudgetParam"},"attributes":{"type":"array","items":{"type":"string"},"maxItems":50,"description":"Output column names; they become the dataset's own enrichment's columns, typed from their names. Not with enrichment_id. Nothing is filled until POST /enrich (or auto_fill)."},"enrichment_id":{"type":"string","description":"A saved enrichment (enr_...) whose output columns become the dataset's. Not with attributes."},"auto_fill":{"type":"boolean","default":false,"description":"With count: fill the output columns when the rows land (a second paid run)."},"webhook_url":{"type":"string","format":"uri","description":"POSTed the list-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"}}},"examples":{"rows_and_attributes":{"summary":"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":"Generate 50 rows on a 100-credit budget, filled when they land, refused above 200 credits","value":{"prompt":"Specialty coffee roasters in San Francisco","count":50,"input_columns":["Roaster","Website"],"attributes":["Founded year"],"auto_fill":true,"budget":100,"max_credits":200}},"from_prompt_alone":{"summary":"The whole dataset from the prompt — identity columns and typed output columns","value":{"prompt":"Top 50 US fintech startups with their CEO, total funding, and founding year","count":50,"auto_fill":true}}}}}},"responses":{"200":{"description":"estimate_cost only — the credit estimate (`upfront` / `expected` charged now; `typical` range; `maximum` = budget — `budget`, else 50 + 1.5 × count — plus every row's budget with auto_fill); nothing was started","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EstimateCostResult"}}}},"201":{"description":"The dataset, with `run` when list generation started (null when the rows came in the request)","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Dataset"},{"type":"object","properties":{"run":{"$ref":"#/components/schemas/Run"}}}]}}}},"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/get/datasets/{id}); rows on [GET /datasets/{id}/rows](#tag/datasets/get/datasets/{id}/rows).\n","operationId":"listDatasets","x-mcp-description":"The account's datasets, newest first (up to 200): id, name, column counts, `row_count`, newest run. `get_dataset` has the columns and enrichment.\n","tags":["Datasets"],"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).\n","operationId":"getDataset","x-mcp-description":"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`.\n","tags":["Datasets"],"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, pin or unpin a tool, 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, schema, or pinned tool 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.\n\nThe budget follows the columns as on [PATCH /enrichments/{id}](#tag/enrich/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.\n","operationId":"updateDataset","x-mcp-destructive":true,"x-mcp-description":"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`; `{ \"tool\": \"<list_tools name>\" }` pins a tool: one call per row, arguments chosen by the agent, result is the cell (`null` unpins); `{ \"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`, `pinned_columns`, `output_schema`.\n","tags":["Datasets"],"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"}}}},"pin_a_tool":{"summary":"A column pinned to the LinkedIn profile lookup; the agent finds the URL to call it on","value":{"output":{"Profile":{"prompt":"The LinkedIn profile of the row's person; find the canonical linkedin.com/in/<slug> URL first","tool":"linkedin_person_profile"}}}},"add_input_column":{"summary":"Rename the dataset, add an input column and dedupe on it, rename another","value":{"name":"Q4 targets","input_columns":{"Ticker":{},"Website":{"rename":"Domain"}},"index_column":"Ticker"}}}}}},"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.\n","operationId":"deleteDataset","x-mcp-destructive":true,"x-mcp-description":"Delete a dataset by id (rows and cells with it; its enrichment stays). 409 `dataset_in_use` while a run is on it.\n","tags":["Datasets"],"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.\n\nTwo parameters make a read small enough to take a whole dataset in a few calls: `columns` (comma-separated names) keeps only those columns in `input` and `cells`, and `compact=true` answers the values-only shape (`CompactDatasetRow`: `position`, `input`, `cells` as `{ \"<column>\": value }`, a cell without a value null). A 1,000-row page of one column in that shape is a few tens of kilobytes. For counts over a column, [GET /datasets/{id}/summary](#tag/datasets/get/datasets/{id}/summary) needs no rows at all.\n","operationId":"listDatasetRows","x-mcp-description":"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. To read a whole dataset: `columns` names the columns, `compact: true` makes rows `{ position, input, cells: { \"<column>\": value } }`; 1,000 rows of one column is one call. For counts of a column's values or states, `get_dataset_summary`.\n","tags":["Datasets"],"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"}},{"$ref":"#/components/parameters/DatasetColumns"},{"name":"compact","in":"query","description":"`true` answers the values-only row shape: `position`, `input`, and `cells` as `{ \"<column>\": value }` (no id, states, sources, credits, or timestamps).","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"One page of rows","content":{"application/json":{"schema":{"type":"object","properties":{"rows":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/DatasetRow"},{"$ref":"#/components/schemas/CompactDatasetRow"}]}},"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/post/datasets/{id}/rows/import).\n","operationId":"addDatasetRows","x-mcp-description":"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`.\n","tags":["Datasets"],"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.\n","operationId":"deleteDatasetRows","x-mcp-destructive":true,"x-mcp-description":"Remove rows: `ids` (comma-separated `row_...` ids) and / or `positions` (comma-separated positions from 1), up to 100 rows together. Returns the number deleted.\n","tags":["Datasets"],"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/get/datasets/{id}) shows `import_in_progress`). Duplicates on the index column are skipped.\n","operationId":"importDatasetRows","x-mcp-description":"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.\n","tags":["Datasets"],"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/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`.\n","operationId":"buildDatasetRows","x-mcp-open-world":true,"x-mcp-description":"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`.\n","tags":["Datasets"],"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."},"budget":{"$ref":"#/components/schemas/ListBuildBudgetParam"},"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":{"200":{"description":"estimate_cost only — the credit estimate; nothing was started","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EstimateCostResult"}}}},"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"}}}]}}}},"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. `input` is refused with 409 `row_in_use` while a fill is on the row; `cells` land during a fill, and a cell set by hand while its row runs stands over the run's answer for it.\n","operationId":"updateDatasetRow","x-mcp-destructive":true,"x-mcp-description":"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` for `input` while a fill is on the row; `cells` land meanwhile and stand over the run's answer.\n","tags":["Datasets"],"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. Converting the last output column leaves the dataset with none; a fill is refused until one is added.\n","operationId":"promoteDatasetColumn","x-mcp-destructive":true,"x-mcp-description":"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.\n","tags":["Datasets"],"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)).\n","operationId":"exportDataset","x-mcp-description":"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); for a whole dataset, `list_dataset_rows` with `columns` + `compact`; for counts, `get_dataset_summary`.\n","tags":["Datasets"],"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}/summary":{"get":{"summary":"summarize columns","description":"Per-column counts over every row of the dataset, without the rows: for an output column, its cells by state (`pending`, `stale`, `failed`, `not_found`, `ok`, `manual`; a fill in flight shows on [GET /datasets/{id}](#tag/datasets/get/datasets/{id}) `runs`, its cells still count under the state they hold) and its most common values; for an input column, the rows with a value and its most common values. `values` holds the `top_values` most common (default 20, max 100) as `{ value, count }`; `distinct_values` and `filled` count every value. A string value longer than 200 characters is cut in `values`. `columns` picks the columns; absent, every column is summarized. Built with one aggregate query per column, so a 10,000-row dataset answers in well under a second.\n","operationId":"getDatasetSummary","x-mcp-description":"Counts over every row of a dataset, no rows read: per column, `states` (an output column's cells: pending / stale / failed / not_found / ok / manual), `filled`, `distinct_values`, and `values` (the `top_values` most common as `{ value, count }`, default 20, max 100; strings cut at 200 characters). The call for \"how many of each\", \"how many are not found\", \"what does this column hold\", and what a fill filled. `columns` picks the columns; absent, all.\n","tags":["Datasets"],"parameters":[{"$ref":"#/components/parameters/DatasetId"},{"$ref":"#/components/parameters/DatasetColumns"},{"name":"top_values","in":"query","description":"How many of a column's most common values to list","schema":{"type":"integer","default":20,"minimum":1,"maximum":100}}],"responses":{"200":{"description":"The summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DatasetSummary"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/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.\n\nReturns the list-build run; poll [GET /runs/{id}](#tag/runs/get/runs/{id}). The rows land in a dataset named by the run's `dataset_id`; fill them with [POST /enrich](#tag/enrich/post/enrich).\n","operationId":"buildConfiguredDataset","x-mcp-open-world":true,"x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst run = await riveter.configuredDatasets.build(\"cds_YOUR_CONFIGURED_DATASET_ID\", {\n  parameters: { City: \"Denver\" },\n});\nconst result = await riveter.runs.waitForResult(run.id);\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\nrun = riveter.configured_datasets.build(\n    \"cds_YOUR_CONFIGURED_DATASET_ID\",\n    parameters={\"City\": \"Denver\"},\n)\nresult = riveter.runs.wait_for_result(run.id)\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\nrun = riveter.configured_datasets.build(\n  \"cds_YOUR_CONFIGURED_DATASET_ID\",\n  parameters: { \"City\" => \"Denver\" }\n)\nresult = riveter.runs.wait_for_result(run.id)\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\nctx := context.Background()\nrun, err := client.ConfiguredDatasets.Build(ctx, \"cds_YOUR_CONFIGURED_DATASET_ID\",\n    riveter.BuildConfiguredDatasetParams{Parameters: map[string]string{\"City\": \"Denver\"}})\nresult, err := client.Runs.WaitForResult(ctx, run.ID, nil)\n"}],"tags":["Configured 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":{"200":{"description":"estimate_cost only — the tier's flat price; nothing was started","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EstimateCostResult"}}}},"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"}}}]}}}},"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`.\n\n`variables` fills any `{{ placeholder }}` values the plan defines (e.g. a search term or location). Each run charges run credits (`credits_charged`).\n","operationId":"runExtraction","x-mcp-open-world":true,"x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst run = await riveter.extractions.run(\"ext_YOUR_EXTRACTION_ID\", {\n  variables: { location: \"Denver\" },\n});\nconst result = await riveter.runs.waitForResult(run.id);\nconsole.log(result.output);\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\nrun = riveter.extractions.run(\n    \"ext_YOUR_EXTRACTION_ID\",\n    variables={\"location\": \"Denver\"},\n)\nresult = riveter.runs.wait_for_result(run.id)\nprint(result.output)\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\nrun = riveter.extractions.run(\n  \"ext_YOUR_EXTRACTION_ID\",\n  variables: { \"location\" => \"Denver\" }\n)\nresult = riveter.runs.wait_for_result(run.id)\nputs result.output\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\nctx := context.Background()\nrun, err := client.Extractions.Run(ctx, \"ext_YOUR_EXTRACTION_ID\",\n    riveter.RunExtractionParams{Variables: map[string]string{\"location\": \"Denver\"}})\nresult, err := client.Runs.WaitForResult(ctx, run.ID, nil)\nfmt.Println(string(result.Output))\n"}],"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":{"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"}}}},"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"}}}]}}}},"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.\n\nPoll [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.\n\nCreating an extraction charges discovery credits (returned as `credits_charged`). To see the price first, send `estimate_cost: true`.\n","operationId":"createExtraction","x-mcp-open-world":true,"x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst extraction = await riveter.extractions.create({\n  starting_url: \"https://example.com/directory\",\n  goal_description: \"Extract every listed company\",\n  output_record_json_schema: {\n    type: \"object\",\n    properties: { name: { type: \"string\" }, website: { type: \"string\" } },\n  },\n});\nconsole.log(extraction.id); // ext_...\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\nextraction = riveter.extractions.create(\n    starting_url=\"https://example.com/directory\",\n    goal_description=\"Extract every listed company\",\n    output_record_json_schema={\n        \"type\": \"object\",\n        \"properties\": {\"name\": {\"type\": \"string\"}, \"website\": {\"type\": \"string\"}},\n    },\n)\nprint(extraction.id)  # ext_...\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\nextraction = riveter.extractions.create(\n  starting_url: \"https://example.com/directory\",\n  goal_description: \"Extract every listed company\",\n  output_record_json_schema: {\n    \"type\" => \"object\",\n    \"properties\" => { \"name\" => { \"type\" => \"string\" }, \"website\" => { \"type\" => \"string\" } }\n  }\n)\nputs extraction.id # ext_...\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\nextraction, err := client.Extractions.Create(context.Background(),\n    riveter.CreateExtractionParams{\n        StartingURL:     \"https://example.com/directory\",\n        GoalDescription: \"Extract every listed company\",\n        OutputRecordJSONSchema: map[string]any{\n            \"type\": \"object\",\n            \"properties\": map[string]any{\n                \"name\":    map[string]any{\"type\": \"string\"},\n                \"website\": map[string]any{\"type\": \"string\"},\n            },\n        },\n    })\nfmt.Println(extraction.ID) // ext_...\n"}],"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":{"200":{"description":"estimate_cost only — the discovery price (type extraction_discovery); nothing was created or charged","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EstimateCostResult"}}}},"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"}}}}}}},"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).\n","operationId":"getExtraction","x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst extraction = await riveter.extractions.get(\"ext_YOUR_EXTRACTION_ID\");\nconsole.log(extraction.status); // \"ready\" once discovery finishes\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\nextraction = riveter.extractions.get(\"ext_YOUR_EXTRACTION_ID\")\nprint(extraction.status)  # \"ready\" once discovery finishes\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\nextraction = riveter.extractions.get(\"ext_YOUR_EXTRACTION_ID\")\nputs extraction.status # \"ready\" once discovery finishes\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\nextraction, err := client.Extractions.Get(context.Background(), \"ext_YOUR_EXTRACTION_ID\")\nfmt.Println(extraction.Status) // \"ready\" once discovery finishes\n"}],"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"}}}},"/monitors":{"post":{"summary":"create monitor","description":"Create a **monitor**: a schedule that fills a dataset's output columns again daily, weekly, or monthly, compares each cell with the previous run, and can POST the results (or only changes) to a webhook. Pass `dataset_id` (or `enrichment_id` naming the dataset's enrichment) and, optionally, `columns` — the output column names each run fills (every one when omitted). The rows are the dataset's: a monitor takes no `input`; add rows with [POST /datasets/{id}/rows](#tag/datasets/post/datasets/{id}/rows).\n\nCreating the monitor charges nothing. Every scheduled run is a fill of the dataset and charges the fill price (rows × the enrichment's per-row budget at most). `estimate_cost: true` returns the credit estimate for **one** scheduled run; `max_credits` refuses the monitor when that estimate is higher. Each run is listed by [GET /monitors/{id}/runs](#tag/monitors/get/monitors/{id}/runs) and read by [GET /runs/{id}/result](#tag/runs/get/runs/{id}/result); with `output_format: include_diffs` every cell carries `previous_value` and `changed`.\n\n`enrichment_id` naming a legacy enrichment creates a monitor on it over fixed `input` rows, as [the legacy API docs](?spec=legacy#tag/monitors/post/monitor_enrichment) describe; the response carries `legacy`, and so does every read of that monitor (`legacy: true` on its item in [GET /monitors](#tag/monitors/get/monitors)). The successor is a monitor on the enrichment's dataset copy (`dataset_id` on its item in [GET /enrichments](#tag/enrich/get/enrichments)).\n","x-mcp-description":"Create a **monitor**: a schedule that fills a dataset's output columns again daily, weekly, or monthly, compares each cell with the previous run, and can POST results (or only changes) to a webhook. Pass `dataset_id` and optionally `columns` (output column names; every one when omitted); the rows are the dataset's, no `input`. Creation is free; every run charges the fill price. `estimate_cost: true` prices one run. `help` → `monitors`.\n","operationId":"createMonitor","x-mcp-open-world":true,"x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst monitor = await riveter.monitors.create({\n  dataset_id: \"ds_YOUR_DATASET_ID\",\n  cadence: \"daily\",\n  minute: 0,\n  hour: 9,\n  timezone: \"America/New_York\",\n  webhook_url: \"https://your-server.com/webhook\",\n});\nconsole.log(monitor.id, monitor.next_run_at);\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\nmonitor = riveter.monitors.create(\n    dataset_id=\"ds_YOUR_DATASET_ID\",\n    cadence=\"daily\",\n    minute=0,\n    hour=9,\n    timezone=\"America/New_York\",\n    webhook_url=\"https://your-server.com/webhook\",\n)\nprint(monitor.id, monitor.next_run_at)\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\nmonitor = riveter.monitors.create(\n  dataset_id: \"ds_YOUR_DATASET_ID\",\n  cadence: \"daily\",\n  minute: 0,\n  hour: 9,\n  timezone: \"America/New_York\",\n  webhook_url: \"https://your-server.com/webhook\"\n)\nputs \"#{monitor.id} #{monitor.next_run_at}\"\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\nmonitor, err := client.Monitors.Create(context.Background(),\n    riveter.CreateMonitorParams{\n        DatasetID:  \"ds_YOUR_DATASET_ID\",\n        Cadence:    \"daily\",\n        Minute:     0,\n        Hour:       9,\n        Timezone:   \"America/New_York\",\n        WebhookURL: \"https://your-server.com/webhook\",\n    })\nfmt.Println(monitor.ID, monitor.NextRunAt)\n"}],"tags":["Monitors"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"dataset_id":{"type":"string","description":"The dataset to fill again on the schedule (ds_...). One of dataset_id / enrichment_id is required."},"columns":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100,"description":"Output column names each run fills. Omit for every output column, including ones added later."},"enrichment_id":{"type":"string","description":"Instead of dataset_id — the enrichment bound to the dataset (enr_...). A legacy enrichment's id creates a monitor on it over fixed `input` rows (legacy API docs); the response carries `legacy`."},"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","on_diff","disabled"],"description":"When the webhook fires — every run, only when a monitored cell changed since the previous run, or never (default each_run)"},"output_format":{"type":"string","enum":["current_only","include_diffs"],"description":"include_diffs adds `previous_value` and `changed` to every cell of the run's output and webhook payload (default current_only)"},"run_immediately":{"type":"boolean","description":"Also run the monitor immediately after creation (a paid run)"},"input":{"$ref":"#/components/schemas/EnrichmentInputData","description":"Monitors on a legacy enrichment only — fixed rows every scheduled run enriches (same shape as enrich input). Refused on a dataset monitor; its rows are the dataset's."},"estimate_cost":{"$ref":"#/components/schemas/EstimateCostParam"},"max_credits":{"$ref":"#/components/schemas/MaxCreditsParam"}},"required":["cadence","minute","hour","timezone"]},"examples":{"daily_dataset_monitor":{"summary":"Daily monitor on a dataset, webhook only on changes","value":{"dataset_id":"ds_018f5b60-1234-7abc-89ab-0123456789ab","columns":["Headcount","Latest funding round"],"cadence":"daily","hour":9,"minute":0,"timezone":"UTC","alert_rule":"on_diff","output_format":"include_diffs","webhook_url":"https://your-server.com/webhook"}},"weekly_every_column":{"summary":"Weekly monitor, every output column, results every run","value":{"dataset_id":"ds_018f5b60-1234-7abc-89ab-0123456789ab","cadence":"weekly","day_of_week":1,"hour":7,"minute":30,"timezone":"America/New_York","webhook_url":"https://your-server.com/webhook"}}}}}},"responses":{"200":{"description":"estimate_cost only — the credit estimate for ONE scheduled run (the dataset's rows × the per-row budget; type monitor); nothing was created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EstimateCostResult"}}}},"201":{"description":"Monitor created (with `legacy` when it runs a legacy enrichment)","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"}}},"get":{"summary":"list monitors","description":"List the account's monitors, newest first. An item whose monitor runs a legacy enrichment carries `legacy: true`.","operationId":"listMonitors","x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst monitors = await riveter.monitors.list();\nfor (const monitor of monitors) {\n  console.log(monitor.id, monitor.schedule_summary);\n}\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\nfor monitor in riveter.monitors.list():\n    print(monitor.id, monitor.schedule_summary)\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\nriveter.monitors.list.each do |monitor|\n  puts \"#{monitor.id} #{monitor.schedule_summary}\"\nend\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\nmonitors, err := client.Monitors.List(context.Background())\nfor _, monitor := range monitors {\n    fmt.Println(monitor.ID, monitor.ScheduleSummary)\n}\n"}],"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, next run time, and the dataset and `columns` it fills. A monitor on a legacy enrichment answers with `legacy`.","operationId":"getMonitor","x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst monitor = await riveter.monitors.get(\"mon_YOUR_MONITOR_ID\");\nconsole.log(monitor.enabled, monitor.next_run_at);\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\nmonitor = riveter.monitors.get(\"mon_YOUR_MONITOR_ID\")\nprint(monitor.enabled, monitor.next_run_at)\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\nmonitor = riveter.monitors.get(\"mon_YOUR_MONITOR_ID\")\nputs \"#{monitor.enabled} #{monitor.next_run_at}\"\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\nmonitor, err := client.Monitors.Get(context.Background(), \"mon_YOUR_MONITOR_ID\")\nfmt.Println(monitor.Enabled, monitor.NextRunAt)\n"}],"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; `columns` changes the output columns each run fills — `null` for every column (refused on a monitor on a legacy enrichment, whose response carries `legacy`).\n","operationId":"updateMonitor","x-mcp-open-world":true,"x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\n// Pause the monitor:\nconst monitor = await riveter.monitors.update(\"mon_YOUR_MONITOR_ID\", {\n  enabled: false,\n});\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\n# Pause the monitor:\nmonitor = riveter.monitors.update(\"mon_YOUR_MONITOR_ID\", enabled=False)\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\n# Pause the monitor:\nmonitor = riveter.monitors.update(\"mon_YOUR_MONITOR_ID\", enabled: false)\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\n// Pause the monitor (Enabled is a pointer so `false` still serializes):\nenabled := false\nmonitor, err := client.Monitors.Update(context.Background(), \"mon_YOUR_MONITOR_ID\",\n    riveter.UpdateMonitorParams{Enabled: &enabled})\n"}],"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"},"columns":{"type":["array","null"],"items":{"type":"string"},"description":"The output column names each run fills; null for every column"}}},"examples":{"pause":{"summary":"Pause","value":{"enabled":false}},"resume":{"summary":"Resume","value":{"enabled":true}},"columns":{"summary":"Fill two columns only","value":{"columns":["Headcount","Latest funding round"]}}}}}},"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`. A monitor on a legacy enrichment answers with `legacy`.\n","operationId":"listMonitorRuns","x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst page = await riveter.monitors.runs(\"mon_YOUR_MONITOR_ID\");\nfor await (const run of page) { // auto-pages through every result\n  console.log(run.id, run.status);\n}\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\npage = riveter.monitors.runs(\"mon_YOUR_MONITOR_ID\")\nfor run in page.auto_paging_iter():  # pages through every result\n    print(run.id, run.status)\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\npage = riveter.monitors.runs(\"mon_YOUR_MONITOR_ID\")\npage.auto_paging_each do |run| # pages through every result\n  puts \"#{run.id} #{run.status}\"\nend\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\nctx := context.Background()\npage, err := client.Monitors.Runs(ctx, \"mon_YOUR_MONITOR_ID\", nil)\nfor {\n    for _, run := range page.Runs {\n        fmt.Println(run.ID, run.Status)\n    }\n    if !page.HasNextPage() {\n        break\n    }\n    page, err = page.NextPage(ctx)\n}\n"}],"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"},"legacy":{"$ref":"#/components/schemas/LegacyNotice"}}}}}},"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.\n\nThe result is also stored on the run, so it stays re-fetchable at [GET /runs/{id}/result](#tag/runs/get/runs/{id}/result).\n\nFor bulk searches — up to **100,000** per request, async with `webhook_url` support — use the legacy [POST /v1/web_search](./openapi.legacy.yaml) endpoint.\n\n## Quick example\n```bash\ncurl -X POST https://api.riveterhq.com/v1/quick_search \\\n  -H \"Authorization: Bearer YOUR_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"query\": \"latest OpenAI news\"}'\n```\n\n## Credit costs\n- **0.04 credits** per search.\n","operationId":"quickSearch","x-mcp-open-world":true,"x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst run = await riveter.quickSearch({ query: \"Riveter data enrichment\" });\nconsole.log(run.output); // synchronous — results are already here\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\nrun = riveter.quick_search(\"Riveter data enrichment\")\nprint(run.output)  # synchronous — results are already here\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\nrun = riveter.quick_search(query: \"Riveter data enrichment\")\nputs run.output # synchronous — results are already here\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\nrun, err := client.QuickSearch(context.Background(),\n    riveter.QuickSearchParams{Query: \"Riveter data enrichment\"})\nfmt.Println(string(run.Output)) // synchronous — results are already here\n"}],"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.\n\nThis 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.\n\nThe 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.\n\nPass `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\"`.\n\n## Quick example\n```bash\ncurl -X POST https://api.riveterhq.com/v1/search_agent \\\n  -H \"Authorization: Bearer YOUR_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"prompt\": \"Who is the current CEO of Anthropic, and when did they take the role?\"}'\n```\n\n## Credit costs\n- **1 credit** per call (same as one agent-mode enrichment cell), charged when the agent completes. Failed runs are not charged.\n","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":{"200":{"description":"estimate_cost only — the flat per-call price; nothing was started","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EstimateCostResult"}}}},"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"}}}]}}}},"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).\n\n## Quick example\n```bash\n  curl -X POST https://api.riveterhq.com/v1/scrape \\\n  -H \"Authorization: Bearer YOUR_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\": \"https://example.com\"}'\n```\n\n## Credit costs\n- **With proxy**: 0.30 credits\n- **Without proxy**: 0.10 credits\n- **From cache**: 0.01 credits\n\n## Proxy usage\nScraping 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').\n\n## Caching\nRecently scraped pages are cached to save credits (0.01 credits on a cache hit). Set `skip_cache: true` to always fetch fresh content.\n","operationId":"scrape","x-mcp-open-world":true,"x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst page = await riveter.scrape({ url: \"https://example.com\" });\nconsole.log(page.text); // synchronous — no run to poll\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\npage = riveter.scrape(\"https://example.com\")\nprint(page.text)  # synchronous — no run to poll\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\npage = riveter.scrape(url: \"https://example.com\")\nputs page.text # synchronous — no run to poll\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\npage, err := client.Scrape(context.Background(),\n    riveter.ScrapeParams{URL: \"https://example.com\"})\nfmt.Println(page.Text) // synchronous — no run to poll\n"}],"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`, or pin to an output column (`tool`): what each does, what a call takes (`inputs`; `input_groups` when any one complete set of them is enough) and gives back (`returns`), its credits per call (`price_note` when the price settles after the call: `pdf` and `image` by the tokens their model spends reading the file, `text_search_pdf` per page, the LinkedIn email / phone tools only on a contact), 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.\n","operationId":"listTools","x-mcp-description":"The tools an enrichment may list in `only_tools` / `additional_tools` or pin to an output column (`tool`): name, what it does, what a call takes (`inputs`, each with `required`; `input_groups` when any one complete set is enough) and gives back (`returns`) — the values a pinned column's row must carry or the agent must find —, credits per call (`price_note` when the price settles after the call — the LinkedIn email / phone tools only on a contact, `text_search_pdf` per page, `pdf` / `image` by tokens spent reading it), 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.\n","tags":["Datasets"],"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).\n","operationId":"getAccount","x-codeSamples":[{"lang":"typescript","label":"TypeScript","source":"import { Riveter } from \"riveter-sdk\";\n\nconst riveter = new Riveter(); // uses env RIVETER_API_KEY\nconst info = await riveter.account();\nconsole.log(info.account.credit.balance);\n"},{"lang":"python","label":"Python","source":"from riveter import Riveter\n\nriveter = Riveter()  # uses env RIVETER_API_KEY\ninfo = riveter.account()\nprint(info.account.credit.balance)\n"},{"lang":"ruby","label":"Ruby","source":"require \"riveter\"\n\nriveter = Riveter::Client.new # uses env RIVETER_API_KEY\ninfo = riveter.account\nputs info.account.credit.balance\n"},{"lang":"go","label":"Go","source":"import riveter \"github.com/riveterhq/riveter-go\"\n\nclient, err := riveter.NewClient() // uses env RIVETER_API_KEY\ninfo, err := client.Account(context.Background())\nfmt.Println(info.Account.Credit.Balance)\n"}],"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"}},"DatasetColumns":{"name":"columns","in":"query","required":false,"description":"Column names, comma-separated (input or output columns; `Company,CEO`). Only those columns are answered. Absent: every column. An unknown name is a 400 that lists the dataset's columns.","schema":{"type":"string"}}},"schemas":{"Run":{"type":"object","description":"The uniform shape for every run, returned by kickoff endpoints and all /runs endpoints.\nRelated resource ids (enrichment_id, dataset_id, extraction_id, monitor_id) appear when they\napply to the run; kickoff responses may add a few endpoint-specific fields.\n","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"},"legacy":{"$ref":"#/components/schemas/LegacyNotice"}},"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"}},"LegacyNotice":{"type":"object","description":"Present on a response when the request took a legacy mode — a legacy enrichment's id on `/enrich`,\n`/enrichments/{id}`, `/monitors`, or `GET /runs`; a monitor on a legacy enrichment; an `output` column\nwith legacy-pipeline fields on `/enrich`; the list-build body on `POST /datasets`. The response also\ncarries `Deprecation: true` and a `Link` header (`rel=\"successor-version\"`). The legacy generation is\ndocumented in [the legacy API docs](?spec=legacy).\n","properties":{"notice":{"type":"string","description":"One or two sentences — what was legacy about the request and what to send instead"},"successor":{"type":"string","description":"`METHOD /v1/path` of the current operation to use"},"docs":{"type":"string","format":"uri","description":"The successor's section of this reference"}},"required":["notice","successor","docs"],"example":{"notice":"enrichment_id named a legacy enrichment, so this run is on the legacy pipeline. It keeps working, but new integrations should use POST /v1/enrich with dataset_id ds_018f5b60-1234-7abc-89ab-0123456789ab.","successor":"POST /v1/enrich","docs":"https://docs.riveterhq.com/#tag/enrich/post/enrich"}},"EstimateCostParam":{"type":"boolean","default":false,"description":"Validate and price the request without running or charging anything; the 200 response is the\ncredit estimate (EstimateCostResult).\n"},"MaxCreditsParam":{"type":"number","minimum":0,"description":"Credit ceiling. Refused with 422 credit_cap_exceeded (nothing charged) when the estimate's\nmaximum exceeds it.\n"},"ListBuildBudgetParam":{"type":"number","minimum":10,"maximum":5000,"description":"Most credits the build may spend (default 50 + 1.5 per row). A deposit of the expected spend is charged at kickoff; the rest is charged as it runs, up to this budget. A low budget means fewer rows."},"CreditEstimate":{"type":"object","description":"Kickoff-time credit estimate. `maximum` is the contract — the run never charges more.\n`minimum` is 0 when the run can charge less (cells that short-circuit, a dataset build settling below its cap).\n`upfront` is the deposit taken at kickoff when `charged_upfront` is true (a dataset build's expected spend; equal to `maximum` for a fixed-price product; 0 for a fill).\n`expected` is the likely charge when history exists (a dataset fill: rows × the enrichment's average charge per row; a dataset build: equal to `upfront`).\n`typical` is `{ low, high }` — the recent p25–p75 settled range for builds of that size (absent elsewhere).\n","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"},"typical":{"type":"object","description":"Recent p25–p75 settled range for builds of this size; absent without a dataset build estimate","properties":{"low":{"type":"number"},"high":{"type":"number"}},"required":["low","high"]},"maximum":{"type":"number","description":"Most the run can cost"},"upfront":{"type":"number","description":"Credits taken at kickoff when charged_upfront is true; 0 when the charge accrues as work completes"},"charged_upfront":{"type":"boolean","description":"true when a deposit 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\nstarted before estimates existed. Estimates and credit errors add `credits_remaining` and `sufficient_credits`.\n","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\nand priced; nothing was created or charged.\n","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.\n`user_request`: stopped through the API or the app. `credit_limit`: the account ran out of credits.\n`error`: the run failed (internal error, or every enrichment cell failed); `error.message` has the detail.\n"},"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).\nAny column header name is allowed; all input columns must have the same array length.\nRow caps: 10,000 rows with enrichment_id, 1,000 rows with an inline config.\n","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`.\nOnly include keys that apply to your chosen format.\n","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 for structured output (object or array root, up to 10 levels; no `$ref`,\n`oneOf`, `allOf`, `not`). Strict mode is applied; mark optional fields nullable. `help` with `formats`.\n"},"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":"One output column of an inline `enrich`: `prompt`, an optional `output_schema`, an optional pinned `tool`\n(the same fields as [OutputColumnSpec](#model/outputcolumnspec), keyed by column name). `contexts` is\naccepted and ignored. Every other field is the legacy pipeline's: a column that sets one keeps the run\non the legacy pipeline (one agent or one tool call per cell, a legacy enrichment) and the response\ncarries `legacy` naming the fields; they are documented in the legacy API docs.\n","properties":{"prompt":{"type":"string","description":"What this cell is, one value. The agent also has the task prompt and the whole row. Leave sources out; the agent records the URLs it cited on the cell's `sources`."},"output_schema":{"type":"object","description":"JSON Schema for the value (type, enum, properties, items, format, ...). Leave it out: the type is inferred from the prompt. Send one only for an enum, a list, or an object. `help` with `output_schema`."},"tool":{"type":"string","description":"Pin a tool by its GET /tools name: one call of it per row is the cell, arguments chosen by the agent (`help` with `pinned_columns`). With tool parameters (`url`, `query`, `code`, ...) it is the legacy tool-only column instead."},"contexts":{"type":"array","description":"Accepted and ignored — the agent reads every column of the row.","items":{"type":"string"}},"tools":{"type":"array","description":"Legacy pipeline: the cell agent's tools (web_search, scrape, pdf, image, http_request, check_urls, text_search_pdf).","items":{"type":"string"}},"max_tool_calls":{"type":"integer","minimum":0,"maximum":10,"description":"Legacy pipeline: cap on tool calls per cell."},"format":{"type":"string","enum":["text","number","url","email","tag","date","json","boolean"],"description":"Legacy pipeline: the value's format. Use `output_schema` instead."},"format_details":{"$ref":"#/components/schemas/FormatDetails"},"run_when":{"type":"string","enum":["always","any_filled","all_filled","dynamic"],"description":"Legacy pipeline: per-row gate (`any_filled` / `all_filled` on the column's dependencies; `dynamic` on `run_when_config`)."},"run_when_config":{"type":"object","description":"Legacy pipeline: rules for `run_when: dynamic` — `match_mode` `all` / `any`, and `rules` on cell values.","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"},"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` take no `value` (\"not found\" counts as empty); the `text_*`\nconditions compare to `value`, case-insensitive except `text_is_exactly`.\n"},"value":{"type":"string","description":"Comparison text for the `text_*` conditions"}},"required":["column","condition"]}}},"example":{"match_mode":"all","rules":[{"column":"CEO","condition":"is_not_empty"},{"column":"Company Size","condition":"text_contains","value":"enterprise"}]}},"url":{"type":"string","description":"Legacy tool-only column: column header or static URL."},"query":{"type":"string","description":"Legacy tool-only column (web_search): column header or static query."},"date_start":{"type":"string","description":"Legacy tool-only column (web_search): start date, YYYY-MM-DD."},"date_end":{"type":"string","description":"Legacy tool-only column (web_search): end date, YYYY-MM-DD."},"code":{"type":"string","description":"Legacy tool-only column (code): JavaScript source. On the dataset engine the prompt describes the computation and the agent writes the code."},"args":{"type":"object","description":"Legacy tool-only column (code): named arguments; values are column headers or static strings."},"proxy_country_code":{"type":"string","description":"Legacy tool-only column (scrape)."},"wait_longer":{"type":"boolean","description":"Legacy tool-only column (scrape)."},"skip_cache":{"type":"boolean","description":"Legacy tool-only column (scrape)."}}},"EnrichmentOutputSpec":{"type":"object","description":"Keys are output column headers. Values are per-column configuration objects. Any output column name\nis allowed that is not an input column. Columns are researched together by one agent per row and need no order.\n","additionalProperties":{"$ref":"#/components/schemas/EnrichmentOutputColumnConfig"},"example":{"Employee Count":{"prompt":"The current number of employees at this company","output_schema":{"type":"integer","minimum":0}},"Industry":{"prompt":"The industry this company is in","output_schema":{"type":"string","enum":["SaaS","Fintech","Healthcare","Other"]}},"CEO":{"prompt":"The CEO's full name"},"CEO Email":{"prompt":"A work email address for the CEO","output_schema":{"type":"string","format":"email"}}}},"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","on_diff","disabled"]},"output_format":{"type":"string","enum":["current_only","include_diffs"]},"next_run_at":{"type":["string","null"],"format":"date-time"},"schedule_summary":{"type":"string","description":"Human-readable schedule (e.g. \"Daily at 9:00 UTC\")"},"dataset_id":{"type":"string","description":"The dataset each run fills (ds_...)"},"dataset_name":{"type":"string","description":"The dataset's name"},"columns":{"type":"array","items":{"type":"string"},"description":"The output column names each run fills (every one when absent)"},"enrichment_id":{"type":"string","description":"The dataset's enrichment, or the legacy enrichment the monitor runs"},"enrichment_name":{"type":"string","description":"Monitors on a legacy enrichment only"},"created_at":{"type":"string","format":"date-time"},"has_input":{"type":"boolean","description":"Monitors on a legacy enrichment only — whether the monitor carries fixed input data"},"legacy":{"oneOf":[{"type":"boolean","description":"`true` on an item of GET /monitors whose monitor runs a legacy enrichment"},{"$ref":"#/components/schemas/LegacyNotice"}],"description":"On GET / PATCH / POST of a monitor on a legacy enrichment, the notice; on its item in GET /monitors, `true`"}}},"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"}},"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, one value, on top of the enrichment's task prompt; on a pinned column, what to call the tool on. Leave sources out; the agent records the URLs it cited on the cell's `sources`."},"output_schema":{"type":"object","description":"A JSON Schema for the cell value (`help` with `output_schema`). Leave it out: the type is inferred from the prompt, caveats included. Send one only for an enum, a list, or an object. Does not apply to a pinned column."},"tool":{"$ref":"#/components/schemas/ColumnToolPin"}},"required":["name"]},"ColumnToolPin":{"type":"string","nullable":true,"description":"A tool name from GET /tools. The row agent makes one call of it per row — arguments it works out from the row, the known values, and its research; the column `prompt` says what to call it on — and the call's result is the cell. `null` unpins. `help` with `pinned_columns`.","example":"linkedin_person_profile"},"OutputColumnOperations":{"type":"object","description":"Output column edits keyed by column name. A name not on the enrichment adds a column; `prompt` / `output_schema` / `tool` change one (its filled cells read `stale`); `rename` renames it (cells kept); `delete: true` removes it.\n","additionalProperties":{"type":"object","properties":{"prompt":{"type":"string","maxLength":5000,"description":"What to find for this column, one value. Leave sources out; the agent records the URLs it cited on the cell's `sources`."},"output_schema":{"type":"object","description":"A JSON Schema for the cell value (`help` with `output_schema`). Leave it out on an added column: the type is inferred from the prompt, caveats included. Send one only for an enum, a list, or an object."},"tool":{"$ref":"#/components/schemas/ColumnToolPin"},"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.\n","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":"An enrichment (the recipe a fill runs). A legacy enrichment's id answers its older shape (the legacy API docs) with `legacy` set.","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"},"legacy":{"$ref":"#/components/schemas/LegacyNotice"}},"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}},{"name":"Homepage text","prompt":"The company's homepage, from the Website column","tool":"scrape"}],"tools":["web_search","scrape","pdf","http_request","code"],"budget_credits_per_row":4.5,"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\n(a failed cell never finished — the next fill re-runs it without force; a not_found cell is an answer and stays);\nmanual = set by hand (PATCH rows/{row_id}); stale = filled before the column's prompt or schema changed (the next fill re-runs it)\n"},"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\nprovider answered with an error or did not answer (run it again); internal = something went wrong on Riveter's\nside (run it again). A stopped run leaves no failed cells: its recorded values land as ok, the rest stay as they were.\n"},"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"}},"CompactDatasetRow":{"type":"object","description":"A row as `compact=true` answers it on GET /datasets/{id}/rows — the values alone. Address the row by `position` elsewhere.","properties":{"position":{"type":"integer","description":"Insertion order, from 1"},"input":{"type":"object","additionalProperties":{"type":"string","nullable":true},"description":"Input values by column name (the `columns` named, else every input column)"},"cells":{"type":"object","additionalProperties":{"nullable":true,"description":"The cell's value; null when the cell has none (pending, not_found, failed)"},"description":"Cell values by output column name (the `columns` named, else every output column)"}},"example":{"position":1,"input":{"Company":"Apple"},"cells":{"CEO":"Tim Cook","HQ city":null}}},"DatasetSummary":{"type":"object","properties":{"row_count":{"type":"integer"},"columns":{"type":"array","items":{"$ref":"#/components/schemas/DatasetColumnSummary"}}},"example":{"row_count":1000,"columns":[{"name":"Company","kind":"input","filled":1000,"distinct_values":1000,"values":[{"value":"Apple","count":1},{"value":"Stripe","count":1}]},{"name":"Phone type","kind":"output","states":{"pending":12,"stale":0,"failed":3,"not_found":185,"ok":800,"manual":0},"filled":800,"distinct_values":2,"values":[{"value":"Mobile","count":610},{"value":"Direct dial","count":190}]}]}},"DatasetColumnSummary":{"type":"object","properties":{"name":{"type":"string"},"kind":{"type":"string","enum":["input","output"]},"states":{"type":"object","description":"Output columns only — the column's cells by state over every row (`running` cells count under the state they hold; the fill in flight is on the dataset's `runs`)","properties":{"pending":{"type":"integer"},"stale":{"type":"integer"},"failed":{"type":"integer"},"not_found":{"type":"integer"},"ok":{"type":"integer"},"manual":{"type":"integer"}}},"filled":{"type":"integer","description":"Rows whose cell (or input value) holds a value"},"distinct_values":{"type":"integer","description":"Distinct values among the filled rows"},"values":{"type":"array","description":"The most common values, most common first, at most `top_values` of them","items":{"type":"object","properties":{"value":{"nullable":true,"description":"The value as stored (a string is cut at 200 characters)"},"count":{"type":"integer"}}}}}},"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, pdf and image to the tokens their model spent reading the file (credits_per_call is what is booked before the call), 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":"A Riveter admin enables it on the account; false for the default tools and the ones any account may add"},"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"},"inputs":{"type":"array","description":"What a call takes. On a pinned column the agent fills these from the row's columns or its research, so the dataset should carry them (a LinkedIn URL column for the LinkedIn tools, a domain for the Hunter domain search).","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","nullable":true,"description":"string / integer / number / boolean / object; null when the input takes more than one shape"},"required":{"type":"boolean"},"description":{"type":"string","nullable":true}}}},"input_groups":{"type":"array","nullable":true,"description":"When set, any one complete group of inputs is enough (`[[\"linkedin_handle\"], [\"domain\", \"full_name\"]]`) and `required` is false on each","items":{"type":"array","items":{"type":"string"}}},"returns":{"type":"string","nullable":true,"description":"What a call gives back — on a pinned column, what the cell holds"}}}},"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 account has not migrated to Datasets yet (`feature_not_enabled` — open Datasets in the app and click \"Migrate to Datasets\")","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.\n\n**The run** (returned by kickoffs and every `/runs` endpoint):\n\n```json\n{\n  \"id\": \"run_018f6a70-1234-7abc-89ab-0123456789ab\",\n  \"type\": \"enrichment\",\n  \"status\": \"processing\",\n  \"progress\": {\n    \"percent_complete\": 40,\n    \"estimated_seconds_remaining\": 90,\n    \"elapsed_seconds\": 60,\n    \"completed_cells\": 4,\n    \"total_cells_expected\": 10,\n    \"not_found_cells\": 0\n  },\n  \"credits_used\": 2.5,\n  \"credit_detail\": {\n    \"estimate\": { \"minimum\": 0, \"maximum\": 10, \"charged_upfront\": false },\n    \"credits_used\": 2.5\n  },\n  \"app_url\": \"https://app.riveterhq.com/runs/run_018f6a70-...\",\n  \"result_url\": \"https://api.riveterhq.com/v1/runs/run_018f6a70-.../result\",\n  \"started_at\": \"2026-01-15T12:00:00Z\",\n  \"finished_at\": null,\n  \"error\": null,\n  \"enrichment_id\": \"enr_018f5b60-...\",\n  \"enrichment_name\": \"My Enrichment\"\n}\n```\n\nRelated resource ids (`enrichment_id`, `dataset_id`, `extraction_id`, `monitor_id`) appear when they apply to the run. A kickoff that took a legacy mode adds `legacy` ([LegacyNotice](#model/legacynotice)).\n"},{"name":"Datasets","description":"Tables 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`). The preferred way to enrich data you have. Start with [POST /datasets](#tag/datasets/post/datasets) — what a dataset is, and three worked flows. On an account that has not migrated to Datasets yet, every endpoint here answers `403 feature_not_enabled` until it does."},{"name":"Enrich","description":"Fill in new columns for rows — a dataset's rows ([POST /enrich](#tag/enrich/post/enrich) with `dataset_id`, the preferred way) or rows sent inline for a one-off — and manage saved enrichment configurations."},{"name":"Extractions","description":"Reusable site scrape/extract recipes and their runs"},{"name":"Tools","description":"Synchronous and quick utilities — web search and scraping."},{"name":"Account","description":"Account and API key information"},{"name":"Monitors","description":"A dataset filled again on a schedule, each cell compared with the previous run, with webhooks"},{"name":"Configured list builds","description":"Templated list builds (`cds_...`): a list generation whose prompt and columns staff configured for your account, run with `parameters` and a `tier`. The rows land in a dataset named by the run's `dataset_id`; fill them with [POST /enrich](#tag/enrich/post/enrich).\n\nFor a list of your own description, use [POST /datasets](#tag/datasets/post/datasets) with `prompt` + `count` or [POST /datasets/{id}/rows/build](#tag/datasets/post/datasets/{id}/rows/build).\n"}]}