Back to Blog
TutorialsOctober 24, 202615 min read

cURL POST JSON: Copy-Paste Examples for Every Method

Working curl commands for POST JSON, PUT, bearer tokens, basic auth, file uploads and GraphQL, plus the quoting fixes for cmd.exe and PowerShell.

cURL POST JSON: Copy-Paste Examples for Every Method

10 min read · Last updated: October 25, 2026

To POST JSON with curl, run curl --json '{"name":"widget"}' https://api.example.com/items. The --json flag, added in curl 7.82.0, sends your data as a POST and sets both Content-Type and Accept to application/json. On older curl, use -H "Content-Type: application/json" -d '{"name":"widget"}'. On Windows, change the quoting or read the body from a file.

Came here from Instagram, LinkedIn, X or YouTube? Here is the full source list you came for. Everything below is built from these, nothing else.

The curl docs

The specs

The Windows docs

Key takeaways

  • --json has existed since curl 7.82.0. It replaces three flags: the data flag, the Content-Type header and the Accept header.
  • Plain -d sends application/x-www-form-urlencoded by default. That one default is why most "my API ignores my JSON" bugs happen.
  • Bearer tokens go in one header: Authorization: Bearer <token>, as defined in RFC 6750 (2012).
  • Basic auth is just base64 of user:password (RFC 7617, 2015). It is cleartext without TLS, so only send it over HTTPS.
  • Windows cmd.exe has no single quotes, and Windows PowerShell 5.1 aliases curl to Invoke-WebRequest. Type curl.exe instead.
  • A GraphQL query is a POST with a JSON body holding one required field, query.

What you'll build and why

By the end of this tutorial you will have a working cURL command for every request you actually send to an API: POST, PUT, PATCH, DELETE, bearer tokens, basic auth, a file upload with JSON metadata, and a GraphQL query.

You will also know why each one breaks, because they mostly break for the same three reasons. The wrong Content-Type, the wrong quotes, or the wrong curl on Windows.

curl, the open-source command-line tool for transferring data over HTTP and many other protocols, is the common language of API docs. Almost every provider writes examples in it. When those examples fail on your machine, the cause is usually your shell, not the API.

Prerequisites

You need three things:

  1. curl 7.82.0 or newer if you want --json. Run curl --version to check. Every example below also shows the older form.
  2. A terminal. Bash or zsh on Linux and macOS, or cmd.exe / PowerShell on Windows. Microsoft says curl is included with Windows and is built from the upstream curl project, "so the same flags and behavior you know from Linux and macOS work the same way on Windows."
  3. An endpoint to hit. Your own API, or any public echo service that returns what you sent. I use https://api.example.com as a placeholder below. Swap in your real URL.

Optional but worth it: jq, a command-line JSON processor. Pipe any response into | jq and it comes back pretty-printed.

Step 1: How do you POST JSON with curl?

Use --json. It sends the data in a POST request and sets Content-Type: application/json and Accept: application/json for you. It shipped in curl 7.82.0. On older versions, pass -d with an explicit Content-Type header. Both forms send the same request; one is shorter.

The modern form:

curl --json '{"name":"widget","price":9.99}' https://api.example.com/items

The classic form, which works on older curl too:

curl -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -d '{"name":"widget","price":9.99}' \
     https://api.example.com/items

Notice what is missing: -X POST. The curl man page says -d sends data "with the POST method", so curl already picks POST. You can add -X POST and nothing changes. It is harmless here, just redundant.

One thing --json does not do: check your JSON. The curl book is explicit: "curl does not touch or parse the data that it sends, so you need to make sure it is valid JSON yourself." A missing brace goes straight to the server.

How do you send JSON from a file?

Put an @ in front of the filename. For a body longer than one line, this is the right default on every operating system.

# from a file
curl --json @body.json https://api.example.com/items

# from stdin
echo '{"a":"b"}' | curl --json @- https://api.example.com/items

# older curl: use --data-binary, not -d
curl -H "Content-Type: application/json" \
     --data-binary @body.json \
     https://api.example.com/items

Why --data-binary and not -d? When -d reads a file, the man page says "carriage returns, newlines and null bytes are stripped out." Pretty-printed JSON usually survives that, because whitespace between tokens is optional. But --data-binary posts "exactly as specified with no extra processing whatsoever." Send the bytes exactly as they are and you never have to think about it.

Step 2: How do you set Content-Type to application/json?

Pass -H "Content-Type: application/json". The man page says a header you set "is used instead of the internal one," so it overrides curl's default form encoding. If you use --json, the header is already set, and you can still override it with -H when an API wants a vendor-specific type.

curl -H "Content-Type: application/json" -d '{"ok":true}' https://api.example.com/hook

Here is what each data flag sends by default:

FlagDefault Content-TypeReads @file?Keeps newlines?Method
-d / --dataapplication/x-www-form-urlencodedYesNo, strips themPOST
--data-binaryapplication/x-www-form-urlencodedYesYesPOST
--data-rawapplication/x-www-form-urlencodedNo, @ is literalYesPOST
--jsonapplication/json (plus Accept)YesYesPOST
-F / --formmultipart/form-dataYes, as a file uploadYesPOST

Read the first row again. If you send JSON with -d and forget the header, curl tells the server it is sending a web form. A JSON API will then either reject the request or read the body as something it is not.

Step 3: What does a curl PUT request look like?

Add -X PUT to any of the POST commands. The man page says -X "only changes the actual word used in the HTTP request, it does not alter the way curl behaves," so the body, the headers and --json all work exactly as before. The same trick covers PATCH and DELETE.

# replace a resource
curl -X PUT --json '{"name":"widget","price":12}' https://api.example.com/items/42

# partial update
curl -X PATCH --json '{"price":12}' https://api.example.com/items/42

# delete, no body
curl -X DELETE https://api.example.com/items/42

There is a second way to PUT. -T (--upload-file) uploads a file, and "if this option is used with an HTTP(S) URL, the PUT method is used." It does not set a JSON content type, so add the header yourself:

curl -T body.json -H "Content-Type: application/json" https://api.example.com/items/42

Use -X PUT --json for API calls, and -T for pushing a file to storage that expects a raw PUT upload.

Step 4: How do you send a bearer token with curl?

Add the header Authorization: Bearer <token>. RFC 6750, the 2012 OAuth 2.0 bearer token spec, says clients SHOULD send tokens this way and resource servers MUST accept it. Put double quotes around the header so your shell expands a $TOKEN variable instead of sending the literal text.

export TOKEN="your-token-here"

curl -H "Authorization: Bearer $TOKEN" \
     --json '{"name":"widget"}' \
     https://api.example.com/items

curl also has a dedicated flag, --oauth2-bearer, which the man page describes as a way to "specify the Bearer Token for OAUTH 2.0 server authentication":

curl --oauth2-bearer "$TOKEN" https://api.example.com/me

Both send the token in an Authorization: Bearer header. I use -H because it reads the same as every API's docs, and it is obvious in a code review.

Keep tokens out of your shell history and screenshots. Load them from an environment variable or a secrets manager instead of pasting them inline. If you are deciding how your own app should issue these tokens in the first place, our Better Auth vs NextAuth comparison covers that side.

Step 5: How do you use basic auth with curl?

Use -u username:password. curl builds the Authorization: Basic header for you by base64-encoding username:password, which is exactly the construction RFC 7617 defines. If you pass only the username, the man page says curl prompts for the password, which keeps it out of your shell history.

# curl builds the header
curl -u alice:s3cret --json '{"ping":1}' https://api.example.com/hooks

# safer: curl prompts for the password
curl -u alice https://api.example.com/account

Under the hood it is a plain header. In RFC 7617's own example, user Aladdin with password open sesame becomes:

Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

Base64 is encoding, not encryption. The RFC says Basic auth is "not considered to be a secure method of user authentication" unless it is paired with something like TLS, because otherwise the credentials travel "as cleartext." Only ever send -u to an https:// URL.

Step 6: How do you upload a file with JSON in one curl request?

Use multipart form data with -F. Put @ before a filename to attach it as a file, or < to read a field's value from a file. Add ;type=application/json so the server knows the metadata part is JSON. curl sends the whole request as multipart/form-data.

curl -H "Authorization: Bearer $TOKEN" \
     -F 'metadata={"title":"Q3 report","public":false};type=application/json' \
     -F "file=@report.pdf;type=application/pdf" \
     https://api.example.com/uploads

If the metadata is long, keep it in a file and use < instead of @. The man page draws the line clearly: @ "makes a file get attached in the post as a file upload," while < "makes a text field and gets the contents for that text field from a file."

curl -F "metadata=<meta.json;type=application/json" \
     -F "file=@photo.jpg" \
     https://api.example.com/uploads

The field names (metadata, file) come from the API you are calling. Check its docs, because there is no standard name.

Do not mix -F with --json or -d in the same request. A request has one body, so you are picking one format: multipart for files plus JSON, or plain JSON on its own.

Step 7: How do you send a GraphQL query with curl?

POST a JSON body with a query field. GraphQL.org's "Serving over HTTP" guide says a standard request uses Content-Type: application/json and a body with query (required), plus optional operationName, variables and extensions. Clients should also send Accept: application/graphql-response+json.

curl -H "Authorization: Bearer $TOKEN" \
     -H "Accept: application/graphql-response+json, application/json" \
     --json '{"query":"query GetItem($id: ID!) { item(id: $id) { name price } }","variables":{"id":"42"}}' \
     https://api.example.com/graphql

Three details make this work:

  1. The query is a string. The whole GraphQL document sits inside "query": "...", so any double quotes inside it need \" escaping. Put values in variables instead of string literals and you rarely need any.
  2. Keep application/json in the Accept header. The guide says that for servers that predate January 1, 2025, clients should include application/json too, so list both.
  3. The response is always JSON, with data, errors or both. A response can carry an errors key, so check for it instead of trusting the status code alone.

For anything longer than one line, write the body to query.json and run curl --json @query.json .... Your future self will thank you.

Why does curl POST JSON fail on Windows?

There are two reasons. cmd.exe has no single quotes, so '{"a":1}' reaches curl broken. And in Windows PowerShell 5.1, curl is an alias for Invoke-WebRequest, a different command with different flags. Type curl.exe, escape the inner double quotes, or read the JSON from a file.

Here is the same request in each shell:

ShellCommand
Bash / zsh (Linux, macOS, WSL, Git Bash)curl --json '{"name":"widget"}' https://api.example.com/items
cmd.execurl --json "{\"name\":\"widget\"}" https://api.example.com/items
PowerShell 7.3+curl --json '{"name":"widget"}' https://api.example.com/items
Windows PowerShell 5.1curl.exe --json '{\"name\":\"widget\"}' https://api.example.com/items
Any shellcurl --json @body.json https://api.example.com/items

The details, from the official docs:

  • cmd.exe. The curl book says that on Windows "there is no support for the single quote version" of quoting. Wrap the JSON in double quotes and escape each inner quote with a backslash. To split a long command across lines, end each line with ^ instead of \.
  • The PowerShell alias. Microsoft's own curl page warns that Windows PowerShell 5.1 "defines a built-in alias named curl that maps to Invoke-WebRequest." Type curl.exe, or remove the alias with Remove-Item Alias:curl. "PowerShell 7+ does not define this alias."
  • PowerShell quote handling. Microsoft's about_Parsing page says PowerShell 7.3 changed how arguments that contain quote characters are passed to native programs, and calls it "a breaking change from the Windows PowerShell 5.1 behavior." On 7.3 and newer, embedded quotes are preserved, so the single-quoted form works. On 5.1 they are not, which is why the 5.1 row escapes them.

The last row is the one I actually use. --json @body.json takes the shell out of the problem completely, on every OS.

Common mistakes that break curl JSON requests

These are the ones I see most, in rough order of how often they cost someone an afternoon:

  1. Using -d without a Content-Type. The server receives a form, not JSON. Fix: use --json, or add -H "Content-Type: application/json".

  2. Single quotes around a variable. 'Bearer $TOKEN' sends the literal text $TOKEN, because shells do not expand variables inside single quotes. Use double quotes for headers that contain variables.

  3. A variable inside single-quoted JSON. It is the same problem, just sneakier. On curl 8.3.0 and newer, command-line variables fix it properly, and the json function quotes the value for you:

    curl --variable msg@message.txt \
         --expand-json '{"text": {{msg:json}}}' \
         https://api.example.com/notes
    
  4. -X GET with -d. This sends a GET with a body. If you meant query parameters, use -G with -d, which the man page says puts the data "in an HTTP GET request instead of the POST request," appended to the URL.

  5. Calling curl in Windows PowerShell 5.1. You are running Invoke-WebRequest. Type curl.exe.

  6. Silent failures in scripts. curl does not treat an HTTP 404 or 500 as a failure unless you ask it to. Add --fail-with-body (curl 7.76.0+): it returns error 22 for HTTP codes 400 and above, but still prints the error body so you can see what the API complained about.

To debug any of these, add -v and read the request headers curl actually sent. That answers "what Content-Type did the server get?" in one line. If your API answers every error with a 200 and an error field, our post on why private APIs can skip HTTP status codes explains when that is a reasonable design, and why --fail-with-body will not catch it.

Next steps

You now have one command per request type. Here is how to make them stick:

  1. Save the ones you use in a scripts folder and commit them next to your code. Our Git commands guide for beginners covers the workflow if version control is new to you.
  2. Move every body into a .json file. It works in every shell and diffs cleanly in review.
  3. Pipe responses into jq to pull out fields instead of scrolling.
  4. Read the man page section on --variable once you start templating requests. It removes most of the quoting tricks above.

FAQ

What is the difference between curl -d and --json?

-d sends a POST with Content-Type: application/x-www-form-urlencoded and strips newlines when it reads a file. --json, added in curl 7.82.0, sends a POST with Content-Type and Accept set to application/json. For JSON APIs, --json is the right default.

Do I need -X POST when sending JSON with curl?

No. -d, --data-binary, --json and -F all make curl use POST on their own. -X POST only changes the method word in the request, so adding it does nothing. You do need -X for PUT, PATCH and DELETE.

How do I see the response status code with curl?

Add -i to print the response headers, including the status line, above the body, or -v to see the full request and response. In scripts, --fail-with-body makes curl return error 22 for any HTTP status of 400 or higher.

Why does my curl JSON work on Mac but not Windows?

Mac and Linux shells support single quotes, which keep the JSON's double quotes intact. Windows cmd.exe does not, and Windows PowerShell 5.1 handles embedded quotes differently and aliases curl to Invoke-WebRequest. Use curl.exe with escaped quotes, or read the body from a file with --json @body.json.

Copy the command you need, swap in your URL, and run it once with -v to see exactly what went over the wire. Check your JSON first with the free tools on Art of Code, and set up your editor with our list of VS Code extensions every developer needs.

Written by Adithya Guttha, Founder of Art of Code.

#curl#api#json#http#command line

Sign in to like and save posts.

Curious Adithya

Written by

Curious Adithya

@curious_adithya · 5 followers

Discussion

Comments (0)

Want to join the conversation?

Sign in to comment

No comments yet. Be the first to comment!