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.

.csv.json.parquet
Drop a file here
Stays in your browserUp to 1.5 GB

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.

curl flags for uploading a file
FlagHTTP methodWhat is sentTypical use
-T filePUTThe file's bytes, unchangedPresigned upload URLs and rows.page
--data-binary @filePOSTThe file's bytes, unchangedAPIs that take a raw POST body
-F 'file=@path'POSTmultipart/form-data, with the file as one form fieldHTML upload forms and multipart APIs
-d @filePOSTThe file with carriage returns and newlines removedForm 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)

bash
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

bash
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.

bash
curl -F 'file=@data.csv' https://example.com/upload

Here 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 orders sets the dataset title.
  • X-Rows-Id-Column: order_id sets the key column used to compare versions.
  • Accept: application/json returns 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.

bash
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:

bash
curl -T data.csv https://rows.page

The 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:

text
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/k3j9x2m1q7

rows.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>:

bash
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:

bash
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

WhatLimit or price
Open a file in your browserFiles 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.