How to Send JSON with cURL: Complete Guide to -d, --json, and Common Mistakes
Specialist in Anti-Bot Strategies
Key Takeaways:
- Sending JSON with cURL is two things, not one. You attach a JSON body to the request, and you tell the server it is JSON via the
Content-Type: application/jsonheader. Skip the header and many APIs reject or misparse the body. -d/--datacarries the payload; the header is on you. The classic pattern iscurl -X POST -H "Content-Type: application/json" -d '{...}' URL.-ddoes not set any JSON header by itself.--jsonis the modern shortcut. Added in curl 7.82.0,--json '{...}'sends the body and sets bothContent-Type: application/jsonandAccept: application/jsonin one flag.- Shell quoting is where most people get burned. Wrap the JSON in single quotes so the shell does not eat the double quotes inside it; on Windows
cmdthe rules differ and a payload file is safer. @filereads the body from disk — but pick the right data flag.-d @body.jsonstrips newlines;--data-binary @body.jsonand--json @body.jsonsend the file byte-for-byte.- The same request shape drives real APIs. A JSON-RPC call to the hosted Scrapeless MCP endpoint is just a POST with a JSON body and an auth header — the exact pattern this guide teaches.
- Free to start. New Scrapeless accounts include free Scraping Browser runtime and residential proxy access — sign up at Scrapeless.
Introduction: the request every API integration starts with
Almost every modern web API speaks JSON. You authenticate with a JSON body, you submit a job with a JSON body, you call a tool on an MCP server with a JSON body. Before any of that runs inside a script or an SDK, it usually starts life as a single curl command in a terminal — the fastest way to confirm an endpoint behaves the way the docs claim.
The trouble is that "send JSON with curl" hides two separate requirements that are easy to conflate. One is attaching the JSON text as the request body. The other is declaring, through the Content-Type header, that the body is JSON so the server parses it correctly instead of treating it as form data. Get the body right but forget the header and a strict API returns a 400 or silently reads nothing. Quote the JSON wrong in your shell and curl sends a mangled string that was never valid JSON to begin with.
This guide defines exactly what "sending JSON with curl" means, walks through the two flag families that do it (-d plus a header, and the newer --json), shows worked examples you can run against a public echo endpoint, and catalogs the mistakes that produce the confusing errors. It closes by mapping the same request shape onto a real call to a JSON API — the hosted Scrapeless MCP endpoint — so the pattern carries straight from the terminal into production. For adjacent background, see our guide to async HTTP scraping with aiohttp and the explainer on what an SSL proxy is.
What "Sending JSON With cURL" Means
cURL (the command-line tool around libcurl) transfers data over HTTP and many other protocols. "Sending JSON with cURL" means issuing an HTTP request — almost always a POST, PUT, or PATCH — whose request body is a JSON document and whose Content-Type header is set to application/json.
Those two parts are independent, and both matter:
- The body is the raw JSON text — for example
{"product":"laptop","max_price":1200}. curl sends these bytes verbatim as the request entity. - The
Content-Typeheader tells the server how to interpret those bytes. Without it, curl's default for-disapplication/x-www-form-urlencoded, the format used for HTML form submissions. A JSON API that sees that header may refuse the request or try (and fail) to parse the body as form fields.
A correct JSON request therefore always pairs a JSON body with the JSON content type. The only question is which curl flags you use to produce that pairing — and that is the difference between the classic -d-plus-header approach and the single-flag --json shortcut covered below.
A quick terminology note: -d is the short form of --data, and -H is the short form of --header. They are interchangeable; this guide uses the short forms in examples and names the long forms where it helps.
Method 1: -d / --data With a Content-Type Header
This is the portable, works-everywhere approach and the one you will see most in API documentation. You supply the body with -d and the header with -H:
bash
curl -X POST https://httpbin.org/post \
-H "Content-Type: application/json" \
-d '{"product":"laptop","max_price":1200}'
Three things are happening:
-X POSTsets the HTTP method. Strictly,-dalready impliesPOST, so-X POSTis optional here — but stating it makes intent explicit and is required if you ever switch the body flag in a way that would otherwise default toGET.-H "Content-Type: application/json"declares the body format.-d '{...}'attaches the JSON. The single quotes keep the shell from interpreting the double quotes inside the JSON.
Running that against httpbin.org/post — a public endpoint that echoes back whatever it receives — returns:
json
{
"data": "{\"product\":\"laptop\",\"max_price\":1200}",
"headers": {
"Accept": "*/*",
"Content-Type": "application/json",
"Host": "httpbin.org",
"User-Agent": "curl/8.18.0"
},
"json": {
"max_price": 1200,
"product": "laptop"
},
"origin": "203.0.113.10",
"url": "https://httpbin.org/post"
}
// Field values are illustrative samples; the structure is what httpbin returns.
The key signal of success is the json object: httpbin only populates it when the body parsed as valid JSON and the Content-Type was application/json. The Accept header is */* — curl's default — because -d does not touch Accept. Note that, by itself, -d sets no JSON header: the Content-Type above is there only because you added the -H line. Drop that line and httpbin would report Content-Type: application/x-www-form-urlencoded and an empty json field.
Method 2: The --json Flag (curl 7.82.0+)
The --json flag arrived in curl 7.82.0 (released in early 2022) to collapse the common case into one option. Check your version with curl --version; if it reports 7.82.0 or newer, --json is available.
bash
curl -X POST https://httpbin.org/post \
--json '{"product":"laptop","max_price":1200}'
A single --json does three jobs at once. It sends the supplied text as the request body, and it sets both of these headers for you:
Content-Type: application/jsonAccept: application/json
That second header is the practical difference from Method 1: --json also tells the server you want JSON back, which some APIs use to choose their response format. Echoing the request through httpbin confirms it:
json
{
"data": "{\"product\":\"laptop\",\"max_price\":1200}",
"headers": {
"Accept": "application/json",
"Content-Type": "application/json",
"Host": "httpbin.org",
"User-Agent": "curl/8.18.0"
},
"json": {
"max_price": 1200,
"product": "laptop"
},
"origin": "203.0.113.10",
"url": "https://httpbin.org/post"
}
// Note both Accept and Content-Type are now application/json.
You can pass --json more than once and curl concatenates the fragments into one body — handy for assembling a payload from pieces. If you need to override one of the headers --json sets (say, a different Accept), add an explicit -H after it; the later header wins.
When should you use each method? Use --json for new work on a current curl. Use -d plus -H when you must support older curl builds, when you want full control over which headers are present, or when the documentation you are following is written that way.
| Behavior | -d '{...}' |
-d '{...}' -H "Content-Type: application/json" |
--json '{...}' |
|---|---|---|---|
| Sends the JSON as the body | Yes | Yes | Yes |
| Default HTTP method | POST | POST | POST |
Sets Content-Type: application/json |
No (defaults to form-urlencoded) | Yes (you set it) | Yes (automatic) |
Sets Accept: application/json |
No | No | Yes (automatic) |
| Minimum curl version | Any | Any | 7.82.0 |
Get your API key on the free plan: Scrapeless
Sending a JSON File With @
Inline JSON gets unwieldy past a few fields, and large payloads belong in a file. Both -d and --json accept the @ prefix to read the body from a path. Given a body.json like:
json
{
"product": "laptop",
"max_price": 1200
}
You can send it with either flag:
bash
# Classic: data flag + explicit header
curl -X POST https://httpbin.org/post \
-H "Content-Type: application/json" \
-d @body.json
# Modern: one flag
curl -X POST https://httpbin.org/post \
--json @body.json
There is a subtle but important difference in how the file is read. -d @body.json strips newlines and carriage returns from the file before sending — a holdover from -d being designed for form data. The body that reaches the server becomes { "product": "laptop", "max_price": 1200}: still valid JSON (whitespace between tokens is allowed), but no longer byte-for-byte what is on disk.
Two flags preserve the file exactly:
bash
# --data-binary keeps every byte, newlines included
curl -X POST https://httpbin.org/post \
-H "Content-Type: application/json" \
--data-binary @body.json
# --json @file also sends the file verbatim
curl -X POST https://httpbin.org/post \
--json @body.json
For ordinary JSON the stripped-newline version still parses, so -d @file usually works. But if the payload must match the file byte-for-byte — a signature is computed over the exact bytes, or the file contains a string value with meaningful embedded newlines — reach for --data-binary @file or --json @file.
You can also pipe a body from stdin by using @-, which is convenient when another program generates the JSON:
bash
generate_payload | curl -X POST https://httpbin.org/post --json @-
Common Mistakes (and How to Avoid Them)
These are the failures that turn a five-second curl into a debugging session.
1. Forgetting the Content-Type header
The most common one. With plain -d and no header, curl sends Content-Type: application/x-www-form-urlencoded. A JSON API then either rejects the request with a 4xx or reads an empty body. Fix: add -H "Content-Type: application/json", or switch to --json, which sets it for you.
2. Shell quoting that breaks the JSON
JSON uses double quotes; most shells also use double quotes for interpolation. Wrapping a payload in double quotes lets the shell strip or expand parts of it before curl ever sees it:
bash
# WRONG on bash/zsh: the shell consumes the inner double quotes
curl -X POST https://httpbin.org/post --json "{"product":"laptop"}"
# RIGHT: single-quote the whole payload
curl -X POST https://httpbin.org/post --json '{"product":"laptop"}'
Fix: wrap the entire JSON document in single quotes on bash/zsh. If a value itself must contain a literal single quote, escape it or move the payload into a file and use @file — which sidesteps shell quoting entirely.
3. Windows cmd quoting differs
Windows cmd.exe does not treat single quotes as quoting characters, so the single-quote trick fails there. You either escape every inner double quote with a backslash, or — far more reliable — put the JSON in a file and send @body.json. PowerShell has its own quoting rules and its curl alias historically pointed at Invoke-WebRequest; call curl.exe explicitly and prefer the @file form to avoid surprises. Fix: on Windows, use a payload file with @body.json.
4. Letting -G turn your body into a query string
-G/--get tells curl to append -d data to the URL as query parameters instead of sending a body. That is the right tool for GET requests, but if you leave it on while trying to POST JSON, your payload silently moves into the URL and the body goes empty. Fix: do not combine -G with a JSON body; use -X POST (or let -d/--json default to POST).
5. Sending invalid JSON
curl does not validate the body — it sends whatever text you give it. A trailing comma, an unquoted key, or a single-quoted string is something the server will reject, often with an opaque parse error. Fix: validate the payload before sending. A quick local check with a JSON parser catches most of it:
bash
# Fails fast on malformed JSON before curl ever runs
echo '{"product":"laptop","max_price":1200}' | python -c "import sys, json; json.load(sys.stdin); print('valid')"
6. Forgetting Accept when the API content-negotiates
Some APIs return XML or HTML unless you ask for JSON. With -d you only set Content-Type, not Accept, so the response may not be JSON even though your request was. Fix: add -H "Accept: application/json", or use --json, which sets Accept for you.
Worked Example: Calling a JSON API
Putting it together, here is the shape of a real JSON API call. The hosted Scrapeless MCP endpoint speaks JSON-RPC over HTTP — which is to say it is exactly the request you have been building: a POST with a JSON body and an auth header. Read the API key from an environment variable so it never appears in your shell history:
bash
# Body lives in init.json; the key comes from the environment, not the command line
curl -X POST "https://api.scrapeless.com/mcp" \
-H "x-api-token: ${SCRAPELESS_API_KEY}" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
--data-binary @init.json
with init.json holding the JSON-RPC handshake:
json
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "curl-demo", "version": "1.0" }
}
}
Every concept from this guide is present: a JSON body (here from a file, sent verbatim with --data-binary), the Content-Type: application/json header that marks it as JSON, an Accept header that names the formats you will accept back, and an auth header carrying the credential. The hosted endpoint exposes roughly two dozen tools — google_search, scrape_html, scrape_markdown, the browser_* automation set, and more — each invoked with the same POST-a-JSON-body pattern, only the method and params change. Set up details live in the Scrapeless docs.
You do not have to keep talking to the endpoint in raw curl, of course — but proving an endpoint with curl first, then porting the verified request into your language of choice, is the workflow that saves the most time. For the MCP server's full tool catalog and worked agent prompts, see 5 Scrapeless MCP use cases.
How Scrapeless Fits
Once a curl command works, the next step is usually to do it at scale — many requests, against sites that render content with JavaScript or screen automated traffic. That is where the request shape you just learned meets managed infrastructure.
Scrapeless provides an anti-detection cloud browser — the Scrapeless Scraping Browser — and residential proxies in 195+ countries, reachable through the hosted MCP endpoint, an SDK, and a CLI. The browser renders JavaScript-heavy pages on the cloud side and manages fingerprints, so the clean JSON request you prototyped in curl returns structured data instead of a challenge page. The transport detail — pinning a residential egress, persisting a session — is handled for you; your side stays the same simple "POST a JSON body, read JSON back" loop.
Explore the Scraping Browser product, review plans on the pricing page, and find the API and MCP reference in the documentation.
Conclusion
Sending JSON with curl comes down to two requirements done together: attach the JSON as the request body, and declare it as JSON with the Content-Type header. The classic way is -d '{...}' plus -H "Content-Type: application/json"; the modern one-flag way is --json '{...}', which sets both Content-Type and Accept for you on curl 7.82.0 and newer. Move large or signed payloads into a file and send them with --data-binary @file or --json @file to preserve every byte, single-quote inline JSON on bash to survive shell quoting, and reach for a payload file on Windows. The same request — body plus content type plus an auth header — is exactly what calling a real JSON API like the Scrapeless MCP endpoint looks like, which is why a curl that works in your terminal ports cleanly into production. For related reading, see async HTTP scraping with aiohttp and what an SSL proxy is.
FAQ
Q: What is the simplest way to send JSON with curl?
On a current curl (7.82.0 or newer), curl --json '{"key":"value"}' URL is the shortest correct form — it sends the body and sets both the Content-Type and Accept headers to application/json. On older curl, use curl -X POST -H "Content-Type: application/json" -d '{"key":"value"}' URL.
Q: Why does my JSON API say the body is missing or invalid even though I sent it?
Two usual causes. Either you sent -d without a Content-Type: application/json header, so the server read it as form data — add the header or use --json. Or your shell mangled the JSON because it was wrapped in double quotes; single-quote the payload, or move it into a file and send @file.
Q: What is the difference between -d, --data-binary, and --json?
-d (--data) sends the body and, for @file, strips newlines; it sets no JSON headers on its own. --data-binary sends the body exactly as given, newlines and all. --json sends the body verbatim and sets Content-Type and Accept to application/json; it requires curl 7.82.0+.
Q: How do I send a JSON file instead of inline text?
Prefix the path with @: curl --json @body.json URL, or curl -H "Content-Type: application/json" --data-binary @body.json URL. Prefer --json @file or --data-binary @file over -d @file when the bytes must match the file exactly, because -d @file removes newlines.
Q: How do I send JSON with curl on Windows?
cmd.exe does not honor single quotes, so the easiest reliable path is to put the JSON in a file and send it with @body.json. If you must inline it, escape every inner double quote with a backslash. In PowerShell, call curl.exe explicitly so you do not hit the Invoke-WebRequest alias, and still prefer the @file form.
Q: Do I need to set the Content-Type header if I use --json?
No. --json sets Content-Type: application/json automatically, along with Accept: application/json. You would only add an explicit header to override one of those — for example a different Accept — in which case put the -H after --json so it takes precedence.
Ready to Build Your AI-Powered Data Pipeline?
Join our community to claim a free plan and connect with developers building JSON-driven data collection pipelines: Discord · Telegram.
Sign up at Scrapeless for free Scraping Browser runtime and residential proxy access, and turn the curl request you prototyped into a production data pipeline.
At Scrapeless, we only access publicly available data while strictly complying with applicable laws, regulations, and website privacy policies. The content in this blog is for demonstration purposes only and does not involve any illegal or infringing activities. We make no guarantees and disclaim all liability for the use of information from this blog or third-party links. Before engaging in any scraping activities, consult your legal advisor and review the target website's terms of service or obtain the necessary permissions.



