Quick answer: To POST JSON with curl, set the content type and pass the body with
-d:curl -X POST https://api.example.com/users -H "Content-Type: application/json" -d '{"name":"Ada"}'. The header is the part people forget; without it curl labels the body as form data and the server answers 415 or 400. For anything longer than a line, put the JSON in a file and send it with-d @body.json.
The endpoint works in Postman. You copy the body into a terminal, run curl, and get 400 Bad Request with no explanation. Nine times out of ten the JSON never arrived the way you typed it: the shell removed the quotes, the header was missing, or the file had a byte order mark. This guide gives you the command that works, then every variation you will need, then the exact errors and what causes them.
Explore the resulting JSON in the JSON Viewer, or select nested fields with the JSONPath Query Tester.
The Canonical curl POST JSON Command
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name": "Ada Lovelace", "email": "ada@example.com"}'
Three flags, each doing one job:
-X POSTsets the method. It is technically redundant because-dalready switches curl from GET to POST, but it makes the intent obvious when you read the command later.-H "Content-Type: application/json"tells the server how to parse the body. Leave it out and curl sendsapplication/x-www-form-urlencoded, which most JSON APIs reject. The Content-Type guide explains whytext/jsonand a charset parameter are not needed.-d '...'is the body. Single quotes on Linux and macOS let the double quotes inside the JSON pass through untouched.
Recent curl releases (7.82.0 and later) add a --json shorthand that sets both the Content-Type and Accept headers for you:
curl --json '{"name": "Ada Lovelace"}' https://api.example.com/users
Check curl --version before relying on it; the system curl on older Linux images and on macOS may predate it.
Send the JSON Body From a File
Inline bodies are fine for a demo and miserable for anything real. Save the payload as body.json and reference it with @:
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d @body.json
This sidesteps every shell quoting problem at once, works identically on Windows, and lets you validate the file before sending. Paste the contents into JSON Prettifier first: a trailing comma or a stray quote is reported with a line and column, which is far more than most APIs tell you.
Two refinements. -d @- reads the body from stdin, so you can generate it with another command. And --data-binary @body.json sends the file byte for byte, where plain -d strips newlines and carriage returns. For JSON the stripping is harmless, since whitespace between tokens is insignificant, but --data-binary is the habit worth forming.
# Generate the body with jq and pipe it straight in
jq -n --arg name "Ada" '{name: $name, active: true}' \
| curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
--data-binary @-
PUT, PATCH, and DELETE With a JSON Body
Only the method changes. -X is required here because the -d default is POST:
# Replace the whole resource
curl -X PUT https://api.example.com/users/42 \
-H "Content-Type: application/json" \
-d '{"name": "Ada Lovelace", "email": "ada@example.com", "active": true}'
# Update some fields (JSON Merge Patch)
curl -X PATCH https://api.example.com/users/42 \
-H "Content-Type: application/merge-patch+json" \
-d '{"active": false}'
# Delete, usually with no body
curl -X DELETE https://api.example.com/users/42
PATCH has two standard body formats, and APIs differ on which one they accept. The JSON Patch vs Merge Patch guide explains the difference and the content types each one expects. If the API only documents application/json for PATCH, use that; the +json suffix types are the more precise option, not a requirement.
Authentication Headers
Headers stack, so add as many -H flags as you need:
# Bearer token (OAuth, JWT)
curl -X POST https://api.example.com/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @body.json
# API key in a custom header
curl -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" -d @body.json https://api.example.com/users
# HTTP Basic auth: curl builds the header for you
curl -u "user:password" -H "Content-Type: application/json" -d @body.json https://api.example.com/users
Two things to watch. Double quotes around the header are deliberate: single quotes would stop the shell from expanding $TOKEN, and you would send the literal text. And an unset variable produces Authorization: Bearer followed by nothing, which servers report as 401, not as a missing header. Run echo $TOKEN before you blame the API.
Windows: cmd.exe and PowerShell Quoting
The Linux command fails on Windows for two unrelated reasons, one per shell.
cmd.exe
cmd.exe does not treat single quotes as quotes at all. Wrap the body in double quotes and escape every inner double quote with a backslash:
curl -X POST https://api.example.com/users -H "Content-Type: application/json" -d "{\"name\": \"Ada\"}"
It works, and it is unreadable past two fields. Use -d @body.json instead.
Windows PowerShell 5.1
Two traps. First, curl is an alias for Invoke-WebRequest, which has completely different flags; you must type curl.exe. Second, when Windows PowerShell passes arguments to a native program it strips embedded double quotes, so '{"name":"Ada"}' arrives at curl as {name:Ada} and the server returns 400. PowerShell 7.3 and later fixed the argument passing, but 5.1 is still the default on most Windows machines.
# Works in Windows PowerShell 5.1: escape the quotes with backslashes inside single quotes
curl.exe -X POST https://api.example.com/users -H "Content-Type: application/json" -d '{\"name\":\"Ada\"}'
# Better: read from a file, no escaping at all
curl.exe -X POST https://api.example.com/users -H "Content-Type: application/json" -d "@body.json"
Or skip curl and use the native cmdlet, which takes a PowerShell object and serializes it for you:
$body = @{ name = "Ada Lovelace"; email = "ada@example.com" } | ConvertTo-Json
$response = Invoke-RestMethod -Uri "https://api.example.com/users" `
-Method Post -ContentType "application/json" -Body $body
$response | ConvertTo-Json -Depth 10
Invoke-RestMethod parses the JSON response into an object automatically. Note the -Depth 10 on the final line: ConvertTo-Json flattens anything below two levels by default, which produces baffling output when you print a nested response.
Pretty-Print the Response
APIs usually return minified JSON, which is unreadable past a few hundred bytes. Pipe into jq, and add -s so curl's progress meter does not land in the middle of it:
curl -s -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d @body.json | jq .
# Or keep the raw response for later
curl -s ... -o response.json
Without jq, copy the response from the terminal and paste it into JSON Prettifier; it formats with syntax highlighting and Sort Keys puts the fields in a predictable order, which helps when comparing two responses. Files up to 5 MB can be uploaded directly. The jq cheat sheet covers extracting single fields when you need the ID from a create response in a script.
Debugging: -i, -v, --fail, and -w
When the response is not what you expect, four flags show you what actually happened:
-iprints the response headers above the body, so you see the status line andContent-Typeof what came back.-vprints the full exchange: lines starting with>are what curl sent,<what the server returned. This is how you confirm yourContent-Typeheader really went out.--fail(or-f) makes curl exit with code 22 on any 4xx or 5xx status instead of printing the error body and exiting 0. Essential in scripts and CI.--fail-with-body(curl 7.76.0 and later) does the same but still shows the body.-w "%{http_code}"prints the status code after the response, which is the cleanest way to branch on it in a shell script.
# Status code only, body discarded
curl -s -o /dev/null -w "%{http_code}" \
-X POST https://api.example.com/users \
-H "Content-Type: application/json" -d @body.json
# See the request headers curl really sent
curl -v -X POST https://api.example.com/users \
-H "Content-Type: application/json" -d @body.json 2>&1 | grep '^>'
Common Errors and What They Mean
Most failures when posting JSON with curl fall into a short list. Match the response to the cause:
| What you see | Usual cause | Fix |
|---|---|---|
415 Unsupported Media Type | No Content-Type header; curl sent application/x-www-form-urlencoded | Add -H "Content-Type: application/json" |
400 Bad Request, body mentions invalid JSON or unexpected token | The shell ate the quotes, or the JSON has single quotes or a trailing comma | Use -d @body.json and validate the file first |
400 with {name:Ada} echoed back | Windows PowerShell 5.1 stripped the double quotes | Use a file, escape with backslashes, or Invoke-RestMethod |
401 Unauthorized | Missing or empty Authorization header ($TOKEN not set) | echo the variable, then -H "Authorization: Bearer $TOKEN" |
405 Method Not Allowed | Wrong method for the route, or a trailing slash redirect turned POST into GET | Check the route; add -L only if the API expects it |
422 Unprocessable Entity | JSON parsed fine but failed validation | Read the error body; fields are usually named |
curl: (3) URL rejected | Unquoted & in the URL split the command | Quote the whole URL |
curl: (6) Could not resolve host | A space after a line-continuation backslash | Delete trailing whitespace after each \ |
When the body is the suspect, run the JSON through a validator before anything else. The common JSON errors guide covers the mistakes a hand-typed body tends to contain, and a well-designed API will tell you which field failed; the JSON error response format guide shows what a good 400 body looks like so you know what to ask your backend team for.
Gotchas
- Trailing spaces after
\. A line continuation with a space after the backslash ends the command early. The next line is then run as its own command, and you getCould not resolve hostfor a hostname like-H. - Multiple
-dflags are joined with&. That is form semantics. For JSON, send one body. - Single quotes inside the JSON.
{'name': 'Ada'}is not JSON, and on Linux it also ends the shell string early. Always double quotes. - Redirects. If the URL redirects, curl follows only with
-L, and a 301 or 302 turns a POST into a GET along the way. Fix the URL rather than adding-L. - Byte order mark in body.json. Files saved by some Windows editors start with an invisible
, and strict parsers reject it. Save as UTF-8 without BOM. - The same request in JavaScript. Once the curl command works, the fetch JSON guide shows the equivalent
fetchcall with the same headers.