Upload a file with curl
How to upload a file with curl: PUT, POST and multipart
curl uploads a file with 3 flags, and curl -T data.csv https://rows.page pushes up to 100 MB for free with no signup: -T sends a PUT, --data-binary @file a raw POST and -F a multipart form. The push prints a link where anyone can filter and query your data.
No file handy? Open CO2 emissions by country and year (50k rows, 14 MB).
Which curl flag uploads a file?
The right flag depends on what the server expects: the raw file as the body of a PUT or POST, or a multipart form like a browser upload.
| Flag | HTTP method | What is sent | Typical use |
|---|---|---|---|
-T file | PUT | The file's bytes, unchanged | Presigned upload URLs and rows.page |
--data-binary @file | POST | The file's bytes, unchanged | APIs that take a raw POST body |
-F 'file=@path' | POST | multipart/form-data, with the file as one form field | HTML upload forms and multipart APIs |
-d @file | POST | The file with carriage returns and newlines removed | Form text, never data files |
-d @file is the classic mistake: it removes line breaks, which breaks CSV and JSONL and corrupts binary files like Parquet. If an API documents a form field name, it wants -F; if it says to send the file as the request body, use -T or --data-binary.
How to upload a file with curl -T (PUT)
curl -T data.csv https://example.com/uploads/When the URL has no file part, curl appends the local file name, so this request goes to /uploads/data.csv. A bare host counts too: curl -T data.csv https://rows.page sends PUT /data.csv. To upload under another name, end the URL with it, like https://example.com/uploads/report.csv.
How to send a file as a raw POST or a multipart form
curl --data-binary @data.csv -H "Content-Type: text/csv" https://example.com/upload--data-binary sends the bytes unchanged but labels them application/x-www-form-urlencoded unless you set Content-Type yourself, as above. -T sends no Content-Type at all.
curl -F 'file=@data.csv' https://example.com/uploadHere file is the form field name the server reads, and the @ tells curl to attach the file instead of sending the literal text. curl sets the multipart/form-data header and its boundary for you.
How to add headers and make curl fail on HTTP errors
Add one -H per header, for example a token in Authorization: Bearer .... rows.page also reads optional push headers:
X-Rows-Title: Nightly orderssets the dataset title.X-Rows-Id-Column: order_idsets the key column used to compare versions.Accept: application/jsonreturns the reply as JSON instead of plain text.
By default, curl does not treat HTTP error codes as failures and exits with 0, so a script carries on after a rejected upload. --fail-with-body, added in curl 7.76.0, exits with code 22 on any status of 400 or higher and still prints the server's error message. -f (--fail) exits the same way but hides that message.
curl -sS --fail-with-body -T data.csv \
-H "Accept: application/json" \
-H "X-Rows-Title: Nightly orders" \
https://rows.page | jq -r .url-s hides the progress meter and -S keeps error messages visible, which suits CI logs. The jq filter prints just the new link.
Try a curl upload with rows.page
rows.page takes a PUT at its root URL with no account or token, for CSV, TSV, JSON, JSONL and Parquet files:
curl -T data.csv https://rows.pageThe plain-text reply gives you the public link, an owner link with a #t= token, a summary line and the command for the next version:
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/k3j9x2m1q7rows.page reads the format from the file name curl appends, so keep the right extension or add ?format=csv, tsv, json, jsonl or parquet to the URL. A POST to https://rows.page, which is what -F and --data-binary send, gets a 405 error, so send raw POST bodies to https://rows.page/api/v1/push instead.
How to push a new version to the same link
To update a link instead of creating a new one, send the rpt_ owner token from your first push to https://rows.page/<id>:
curl -T data.csv -H "Authorization: Bearer rpt_..." https://rows.page/<id>Pushes become v1, v2, v3 on the same link, and identical bytes create no new version. When the previous version is 64 MB or less, a banner shows "+N new, N gone, N changed vs vN", with rows matched on the X-Rows-Id-Column key.
For CI, sign in with Google (the $0 Free plan works) and create an rpk_ API key, up to 20 per account. With ?key=<name>, every run updates the same dataset:
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"rows.page upload limits and rejected files
- Without an account: 100 MB per request, 50 pushes and 1 GB per IP address per UTC day, up to 20 versions, and links expire after 7 days.
- Free ($0): 10 hosted datasets that don't expire, files up to 100 MB.
- Pro ($12 per month) and Max ($39 per month): 100 and 1,000 datasets, files up to 500 MB and 2 GB.
A single request is capped at 100 MB on every plan, so Pro and Max send bigger files through multipart uploads at POST /api/v1/uploads. Over a limit, rows.page answers 413 for a file that is too large, and 429 at the daily cap with a Retry-After header that counts down to UTC midnight.
Excel and zip files are refused with "This looks like an Excel or zip file. Export it as CSV and push again." Gzip files must be decompressed first, and rows.page does not read XML, YAML, PDF or SQLite files.
rows.page limits and prices
| What | Limit or price |
|---|---|
| Open a file in your browser | Files up to 1.5 GB, $0, no account, nothing uploaded |
| Share a link without an account | $0, files up to 100 MB, link deleted 7 days after creation |
| Free account | $0, 10 datasets kept, last 3 versions |
| Pro | $12/month, 100 datasets, files up to 500 MB, private links |
| Max | $39/month, 1,000 datasets, files up to 2 GB, private links |
Questions
What is the difference between curl -d and --data-binary when uploading a file?
-d @file removes carriage returns and newlines before sending, which breaks CSV and JSONL and corrupts binary files like Parquet. --data-binary @file sends the exact bytes. Both send a POST labeled application/x-www-form-urlencoded unless you set Content-Type.
Why does curl -T add my file name to the URL?
curl appends the local file name when the URL has no file part, which means a URL ending in / or a bare host. curl -T data.csv https://example.com/uploads/ uploads to /uploads/data.csv, so end the URL with a file name to choose your own.
Can I upload an Excel .xlsx file to rows.page with curl?
No. rows.page does not read .xlsx or .xls files, and pushing an .xlsx returns "This looks like an Excel or zip file. Export it as CSV and push again." Save the sheet as CSV first, or copy the cells and use Paste data in the browser.
How large a file can I upload to rows.page with curl?
One request takes up to 100 MB on every plan, and without an account you also get 50 pushes and 1 GB per IP address per UTC day. Pro and Max accept files up to 500 MB and 2 GB through multipart uploads at POST /api/v1/uploads.
Why does curl -F fail on rows.page?
rows.page wants the raw file as the request body. curl -F sends a multipart POST, which the root URL rejects with 405 "Use GET, HEAD.", so use curl -T data.csv https://rows.page, or --data-binary to https://rows.page/api/v1/push.