Push a file, get a link
rows.page takes CSV, TSV, JSON, JSONL and Parquet over plain HTTP. No SDK, and no account for the first push.
Push with curl
curl -T data.csv https://rows.pageIn a terminal you get plain text back:
https://rows.page/k3j9x2m1q7
owner link (keep, delete, re-push): https://rows.page/k3j9x2m1q7#t=rpt_...
48,210 rows x 12 columns, csv, 1.2 MB, expires 2026-10-06. Open the owner link and sign in to keep it.
re-push: curl -T data.csv -H "Authorization: Bearer rpt_..." https://rows.page/k3j9x2m1q7The first line is the link to share. The owner link adds #t= plus the dataset token: whoever opens it can keep, rename, delete and pin findings. Browsers never send the part after # to a server, so the token stays out of logs.
JSON responses
Scripts and agents get JSON with Accept: application/json, ?json=1, or by POSTing the raw body to https://rows.page/api/v1/push.
curl -T data.csv -H "Accept: application/json" https://rows.page{
"id": "k3j9x2m1q7",
"version": 1,
"url": "https://rows.page/k3j9x2m1q7",
"owner_url": "https://rows.page/k3j9x2m1q7#t=rpt_...",
"token": "rpt_...",
"format": "csv",
"bytes": 1258291,
"rows": 48210,
"rows_exact": true,
"columns": [
{ "name": "order_id", "type": "integer" },
{ "name": "country", "type": "string" },
{ "name": "amount", "type": "float" }
],
"expires_at": "2026-10-06T09:12:44Z",
"unchanged": false,
"changes": null,
"keep": {
"required": true,
"url": "https://rows.page/k3j9x2m1q7#t=rpt_...",
"message": "Open the owner link and sign in to keep it."
},
"limits": {
"plan": "anon",
"max_file_bytes": 104857600,
"datasets_used": null,
"datasets_limit": null
}
}Store token. It's only returned when the dataset is created.
Keep a dataset
Anonymous datasets are deleted 7 days after creation. To keep one, open the owner link, sign in with Google and click Keep. Pushes made with an API key belong to your account from the start.
New versions
Push to the dataset's URL with its token and you get version 2, 3, and so on. Versions are immutable. The same bytes again add nothing and answer unchanged: true.
curl -T data.csv -H "Authorization: Bearer rpt_..." https://rows.page/k3j9x2m1q7With an API key you can push by your own key instead of tracking ids. The first push creates the dataset, later pushes add versions.
curl --data-binary @orders.csv \
-H "Authorization: Bearer rpk_..." \
"https://rows.page/api/v1/push?key=nightly-orders&id_column=order_id&title=Nightly%20orders"The page compares each version with the one before and counts the rows that are new, gone and changed. Set id_column so changed rows can be matched by id.
Parameters
Pass these in the query string or as X-Rows-* headers, like X-Rows-Title.
- format
csv,tsv,json,jsonlorparquet. Otherwise taken from the file extension, then sniffed from the content.- name
- File name for the version, for requests that don't carry one (like
POST /api/v1/push). - title
- Dataset title shown on the page.
- summary
- A few sentences shown above the findings. The page opens on it.
- key
- Your own stable name, like
nightly-orders. Pushing the same key again adds a version to that dataset. Needs an API key. - id_column
- Column that identifies a row, so the page can count changed rows, not only new and gone ones.
- source
- Where the data came from: a URL, a system, a query. Shown as provenance.
- run
- Job or run id, shown as provenance.
- commit
- Git commit that produced the data, shown as provenance.
- visibility
public(default) orprivate. Private needs Pro or Max.
curl -T results.jsonl \
-H "X-Rows-Title: Eval run 412" \
-H "X-Rows-Run: 412" \
-H "X-Rows-Commit: $(git rev-parse HEAD)" \
https://rows.pageFindings
A finding is a saved view with a title and a note: a filter q, a sort like -amount,name, the columns to show, or a SQL query. A dataset with findings opens on them, so pin what your reader should see first.
curl -X POST https://rows.page/api/v1/datasets/k3j9x2m1q7/findings \
-H "Authorization: Bearer rpt_..." \
-H "content-type: application/json" \
-d '{"title":"Refunds over $500 in NL","note":"All from one merchant.","q":"country:NL amount:>500","sort":"-amount"}'Query language
The filter bar on every dataset and the q of a finding use the same syntax.
- refund "late fee"
- Case-insensitive match in any text column.
- country:NL city:"New York"
- Field matches a value.
- -status:ok
- Exclude matches.
- email:* -email:*
- Has a value, or is empty.
- sku:AB*
- Starts with.
- amount:>500 amount:<=20
- Compare with
>,>=,<or<=. - amount:10..20
- Range.
- at:>2026-09-01
- Dates and times as ISO 8601.
- payload.customer.country:NL
- Nested fields by dot path.
- tags:urgent
- List contains a value.
- (country:NL OR country:BE) amount:>500
- Terms all have to match. Use
ORand parentheses for alternatives. - SELECT ... FROM data
- A query starting with
SELECT,WITHorFROMruns as DuckDB SQL on the tabledata.
Read data back
GET /api/v1/datasets/<id> returns meta, versions, findings and preview rows. /<id>/raw.csv serves the latest version's file and /<id>/v2/raw.csv a fixed one, so DuckDB, pandas, polars or curl read it straight from the URL. The extension is the pushed format: csv, tsv, json, jsonl or parquet.
curl https://rows.page/api/v1/datasets/k3j9x2m1q7
duckdb -c "SELECT count(*) FROM 'https://rows.page/k3j9x2m1q7/raw.csv'"Limits
- Anonymous
- 100 MB per file, deleted 7 days after creation unless kept, up to 20 versions, 50 pushes and 1 GB per day per IP.
- Free
- 10 datasets, 100 MB per file, last 3 versions.
- Pro
- 100 datasets, 500 MB per file, 90 days of versions, private datasets.
- Max
- 1,000 datasets, 2 GB per file, 1 year of versions, private datasets.
- Rate limits
- Per minute: anonymous pushes 10, pushes with a key 120, MCP calls 60, remote proxy 20.
- Big files
- One request carries up to 100 MB. Pro and Max upload bigger files in 64 MB parts.
- In the browser
- About 1.5 GB of data per tab. Past that you get a clear message instead of a crashed tab.
Plans and prices are on the pricing page.