What data is your app missing?

Send a prompt to our API. Get the rows back as JSON. Every cell carries its source URL and the quote that backs it.

Your first 500 credits are free. No card.

  1. 1
    Ask in plain wordsOne POST starts a run. It returns at once, with a time estimate.
  2. 2
    Wait, or let us call youA run takes minutes. Check back when we say, or get a webhook.
  3. 3
    Read rows with sourcesEach cell has a value, a source URL and a quote.

The whole loop fits in one file.

Start a run. Wait as long as the server says. Store each value next to its source.

  • checkAfterSeconds tells you when to ask again. The wait call holds for up to 55 seconds.
  • A run can stop to ask a question, such as how many rows. You answer with one POST.
  • Send an idempotencyKey. A retry gets the same run, not a second one.
  • Big table? get_dataset gives you one dataset.zip of any size.
const API = "https://api.webtotable.app/v1";const headers = {  Authorization: `Bearer ${process.env.WEBTOTABLE_API_KEY}`,  "Content-Type": "application/json",};const get = (path: string) =>  fetch(API + path, { headers }).then((r) => r.json());const sleep = (seconds: number) =>  new Promise((done) => setTimeout(done, seconds * 1000)); // 1. Start a run. It returns at once.const run = await fetch(`${API}/research`, {  method: "POST",  headers,  body: JSON.stringify({    prompt: "25 bouldering gyms in Colorado, " +      "with day-pass price and opening year",    cap: 400, // the most credits this run may use    idempotencyKey: "gyms-co-v1", // a retry gets the same run  }),}).then((r) => r.json());if (!run.runId) throw new Error(run.error.message); // 2. Wait as long as the server says.let res = run;while (!res.done) {  await sleep(res.checkAfterSeconds);  res = await get(`/runs/${run.runId}/wait`);  if (res.status === "waiting_for_user") {    // Ask your user, then POST /v1/runs/{runId}/answer    await answer(run.runId, res.question);  }}if (res.status !== "completed") throw new Error(res.error); // 3. Store each value with its source.for (const row of res.table.rows) {  const pass = row.cells["Day pass (USD)"];  await db.insert(gyms).values({    name: row.cells["Gym"].value,    dayPassUsd: pass.value,    dayPassSource: pass.sourceUrl,  });}

Three things you can build with it.

Directories, marketplaces, niche apps

Fill your app’s database

Ask for the rows your product needs. Store the source next to each value, so your users can check it too.

seed.sql
insert into gyms  (name, day_pass_usd, source)values  ('Granite Loft', 24,   'https://graniteloft.com/rates'),  ('Mesa Crimp House', 22,   'https://mesacrimp.com/pricing'),  -- 23 more, each with its source
Claude Code, Cursor, Codex

Give your agent a research tool

When your coding agent does not know, it guesses. With our MCP server, it asks us and gets facts it can cite.

claude
> Find 30 Postgres extensions with  license and latest release.  Save extensions.json ● webtotable · research  about 8 minutes · cap 400 credits● webtotable · wait_for_table  30 rows · 30 sources✓ Wrote extensions.json  each entry keeps its sourceUrl
Internal tools, CRMs, ops scripts

Fill in the records you have

Upload your CSV. Ask for the columns you are missing. Each new value comes with its source.

POST /v1/files {"name": "accounts.csv", …}POST /v1/research {"files": ["kd71…"], "prompt": "Add team size and HQ city"}
CompanyTeam sizeHQ city
Brightlane60–80Denver
Quillstack25–40Austin
Fernway100–150Toronto

Add it to your agent in one line.

One MCP server. Your agent signs in with OAuth, so you paste no key. The same 35 tools work over REST.

  • We tell your agent to check your credits before it starts.
  • It tells you the estimate and asks before it spends.
  • It waits the time the server gives, then gets the table.
researchwait_for_tableget_tableget_cell_proofsearch_rowsget_changesupload_fileset_scheduleset_webhooksend_to
  1. In your terminal, run:

    claude mcp add --transport http webtotable https://api.webtotable.app/mcp/v1
  2. Open Claude Code, type /mcp, select webtotable and choose Authenticate.

  3. Sign in to Web to Table in your browser and choose Allow.

Every value shows its source.

Not a list of links at the end. The page and the words on it, for each cell.

  • Each cell has sourceUrl and quote. The CSV link has a <column>_source column for each column.
  • We check cells against their source. Each check has a status, such as supported or contradicted.
  • A number from a PDF comes with its page and its box on that page.
  • An empty cell means no source said it.
GET/v1/tables/{id}/cells/proof?rowId=r4&column=Day%20pass%20(USD)
{  "value": 24,  "quote": "Adult day pass $24. Shoes $5.",  "sourceUrl": "https://graniteloft.com/rates",  "host": "graniteloft.com",  "check": {    "status": "supported",    "explanation": "The page states this price.",    "checkedAt": 1792915200000  },  "history": [{    "change": "changed", "before": 22, "after": 24,    "kind": "check", "at": 1792306800000  }]}

Fresh data without a cron job.

Turn on checks once. We recheck every cited cell on your schedule and look for new rows. When something changes, we call your webhook.

1Turn it on

Pick how often. Set a credit cap per check and per month.

POST/v1/tables/{id}/schedule
{  "enabled": true,  "recheck": "daily",  "discover": "weekly",  "capPerCheck": 40,  "capPerMonth": 600,  "alertRules": [{    "text": "when a day pass price changes",    "kind": "cell_changed",    "column": "Day pass (USD)"  }]}

2We call you

A signed POST to your URL when a new version lands or a rule matches.

EVENTtable.changed
{  "id": "evt_4Qm9…",  "type": "table.changed",  "createdAt": 1792920600000,  "data": {    "trigger": "version",    "tableId": "jh72…",    "title": "Bouldering gyms in Colorado",    "version": 7,    "kind": "check",    "changeCount": 3,    "links": { "table": "…", … }  }}

3Read the diff

Rows added, rows removed, and each changed cell, before and after.

GET/v1/tables/{id}/changes?since=6
{  "counts": {    "added": 1, "removed": 0, "changed": 2  },  "changed": [{    "id": "r4",    "cells": { "Day pass (USD)": {      "before": { "value": 22, … },      "after": { "value": 24, … }    } }  }, …],  "added": [ … ], "removed": []}
SignedHMAC-SHA256 in Webhook-Signature. Copy-paste checks for Node and Python.
RetriedAfter 1, 5, 15 and 40 minutes. Each event keeps one Webhook-Id.
Seven eventsRun completed, failed, stopped or asks. Plan proposed. Table changed. Watch triggered.
WatchesWe read a page, feed or sitemap every few hours. New items start a check early.

No webhook server? Point a script or a dashboard at the live table: GET /v1/living/{publicId} for JSON, or a CSV link with a _source column for each column.

The fine print, up front.

Know the edges before you build on it.

Minutes a run

Research is async. A run reads many pages, so it takes minutes. The API gives you the estimate.

You set the cap

A run stops at its credit cap. You set it per call, up to 5,000 credits.

30 calls a minute

Per key. 60 for the whole account. A 429 tells you how long to wait.

3 scopes

tables:read, tables:write and research:run. Only the last one spends credits. Keys last up to 90 days.

Any size

Read 500 rows a page, or get one dataset.zip with every value, its source and a codebook.

Free to read

Reading tables, waiting, proofs, diffs and webhooks use no credits.

A row typically costs 6 to 75 credits. 100 credits cost $1 at the top-up price. Plans start at $97.99 a month, and your first 500 credits are free.

See pricing

So, what data is your app missing?

Make a key. Send one prompt. Read the rows, each with its source.

Get an API key$ claude mcp add --transport http webtotable https://api.webtotable.app/mcp/v1