← All articles

Tutorials

cURL POST Request: Send JSON, Form Data & Files (2026)

Alex R. · May 10, 2026 · 9 min read


Quick verdict: Use -d '{"key":"value"}' -H "Content-Type: application/json" for JSON. Use -F "field=value" -F "file=@photo.jpg" for multipart file uploads. Use -d "name=alice&age=30" for form-encoded data. cURL adds -X POST automatically when you use -d or -F, so you rarely need it explicitly.

POST JSON

The most common pattern in 2026:

curl -X POST https://api.example.com/users      -H "Content-Type: application/json"      -d '{"name":"Alice","email":"alice@example.com"}'

Three things to know:

For complex JSON, read from a file:

curl -X POST https://api.example.com/users      -H "Content-Type: application/json"      -d @user.json

The @ prefix tells cURL "read body from this file." Useful when JSON has special chars or is generated by another tool (jq, etc.).

POST Form-Encoded Data

Old-school HTML form submission:

curl -X POST https://example.com/login      -d "username=alice"      -d "password=secret123"

Multiple -d flags concatenate with &. Equivalent to:

curl -X POST https://example.com/login      -d "username=alice&password=secret123"

Content-Type defaults to application/x-www-form-urlencoded. The body is exactly the encoded string (no JSON wrapping).

URL-Encoded Special Characters

If your form values have spaces, ampersands, or equal signs, use --data-urlencode:

curl -X POST https://example.com/search      --data-urlencode "query=hello world & more"      --data-urlencode "page=1"

cURL escapes the space, ampersand, and other unsafe chars before sending. Without --data-urlencode, the unescaped & would split your value mid-string.

Multipart File Upload

Forms with file inputs use multipart/form-data. Use -F:

curl -X POST https://api.example.com/upload      -F "title=Beach photo"      -F "file=@/home/alice/photo.jpg"

The @ in file=@path is the file-upload syntax. cURL reads the file, sets the right MIME type, and builds the multipart body for you.

Override the MIME type:

-F "file=@photo.jpg;type=image/jpeg"

Set the filename in the form (different from the disk path):

-F "file=@/tmp/upload.bin;filename=document.pdf;type=application/pdf"

Multiple files at once:

curl -X POST https://api.example.com/upload      -F "image1=@photo1.jpg"      -F "image2=@photo2.jpg"

Raw POST Body (no encoding)

Sometimes you want the body sent verbatim, without cURL stripping newlines or interpreting @:

curl -X POST https://api.example.com/raw      -H "Content-Type: text/plain"      --data-raw "$(cat body.txt)"

--data-raw is identical to -d except it does NOT interpret @ as "read from file." Useful when your body genuinely starts with an @ sign (uncommon but exists).

For binary data:

curl -X POST https://api.example.com/binary      -H "Content-Type: application/octet-stream"      --data-binary @image.png

--data-binary @file reads the file byte-for-byte. -d @file strips line endings, which corrupts binary data.

POST with Auth

Bearer token + JSON body:

curl -X POST https://api.example.com/items      -H "Authorization: Bearer eyJhbGc..."      -H "Content-Type: application/json"      -d '{"name":"widget"}'

Basic auth + form data:

curl -X POST https://api.example.com/items      -u user:pass      -d "name=widget"

See cURL basic authentication for all the auth options.

POST Through a Proxy

Useful for testing geo-locked APIs or routing through residential IPs:

curl -X POST https://api.example.com/data      --proxy http://USER:PASS@gw.spyderproxy.com:8000      -H "Content-Type: application/json"      -d '{"region":"us-east"}'

For high-volume API calls behind anti-bot protection, route through Premium Residential proxies for natural-looking traffic.

Capturing the Response

By default cURL prints the body to stdout. Capture it:

# Save to file
curl -X POST https://api.example.com/data -d '...' -o response.json

# Variable in bash
RESPONSE=$(curl -X POST https://api.example.com/data -d '...')

# Get HTTP status code
STATUS=$(curl -X POST https://api.example.com/data -d '...'          -o /dev/null -w "%{http_code}")

Pipe through jq for JSON pretty-printing:

curl -s -X POST https://api.example.com/data -d '{}' | jq .

Debugging POSTs

The -v flag shows the full request and response:

curl -v -X POST https://api.example.com/data -d '{"foo":1}'

You will see lines starting with > (request) and < (response). Critical things to check:

Use --trace-ascii /dev/stdout for even more detail — shows the bytes cURL sent on the wire.

curl vs wget compares the two download tools. cURL timeout settings covers --max-time and --connect-timeout. Mastering cURL is the full reference. For the JavaScript equivalent, see cURL in JavaScript.

Scraping at scale without getting blocked?

See residential proxies ↗Start now ↗