How to POST JSON with cURL in 2026: Inline, File, and jq
Learn the correct way to POST JSON with cURL using inline data, files, stdin, and jq—including headers, compatibility, errors, and safe automation.
Direct answer: with curl 7.82.0 or newer, use --json:
curl --json '{"name":"Ada"}' https://api.example.test/endpoint
Send a JSON document with @file:
curl --json @payload.json https://api.example.test/endpoint
Generate JSON safely with jq and pipe it through standard input:
jq -n --arg name "$NAME" '{name:$name}' |
curl --json @- https://api.example.test/endpoint
--json combines binary data transfer with Content-Type: application/json and Accept: application/json. It does not validate the JSON syntax, so the body must already be valid. See the curl man page for the option definition and compatibility details.
What “POST JSON” means in curl
HTTP separates the method, request body, and headers. curl sends a POST when you provide request data with options such as --json, --data, or --data-binary (unless you override the method). The body bytes and the Content-Type header are separate concerns.
Content-Type: application/jsontells the server how to parse the request body.Accept: application/jsonasks for a JSON response; it does not describe the request body.--datadefaults toapplication/x-www-form-urlencoded, so it is not automatically JSON.--data-binarypreserves supplied bytes but also needs an explicit JSON content type.
1. POST a small, fixed JSON object inline
curl --json '{"active":true,"count":3}' \
https://api.example.test/endpoint
In POSIX shells, single quotes keep the JSON double quotes intact. Add authentication and inspect the response when needed:
curl --fail-with-body --silent --show-error \
--json '{"active":true,"count":3}' \
-H 'Authorization: Bearer YOUR_TOKEN' \
https://api.example.test/endpoint
Use --request POST only when you need to make the method explicit; supplying --json already causes curl to send request data as a POST.
Shell quoting edge cases
Do not concatenate unescaped shell variables into JSON:
# Fragile: quotes, backslashes, and newlines can break the JSON
curl --json "{\"name\":\"$NAME\"}" https://api.example.test/endpoint
Use jq for values that come from the environment, files, or users. It applies JSON escaping for quotes, backslashes, control characters, and Unicode.
2. POST an existing JSON file
For a file that already contains valid JSON, use the @ prefix:
curl --json @payload.json \
https://api.example.test/endpoint
curl reads the file as the complete request body. Standard input works with @-:
curl --json @- https://api.example.test/endpoint < payload.json
These forms are useful in scripts because they avoid shell interpolation and preserve the document produced by another process.
Older curl versions
--json was added in curl 7.82.0. Check the installed version:
curl --version
On older releases, preserve file bytes and set the request content type yourself:
curl -H 'Content-Type: application/json' \
--data-binary @payload.json \
https://api.example.test/endpoint
--data-binary preserves newlines and carriage returns. By contrast, file input with --data strips carriage returns, newlines, and null bytes, and its default content type is form encoded. The curl documentation describes these differences.
3. Build JSON with jq
Use jq -n to construct a document without depending on shell quoting.
NAME='Ada Lovelace'
jq -n --arg name "$NAME" '{name:$name}' |
curl --json @- https://api.example.test/endpoint
--arg always creates a JSON string. Use --argjson when the argument text is already JSON and should remain a number, boolean, array, object, or null:
COUNT=3
ENABLED=true
jq -n \
--arg name "$NAME" \
--argjson count "$COUNT" \
--argjson active "$ENABLED" \
'{name:$name,count:$count,active:$active}' |
curl --json @- https://api.example.test/endpoint
With jq 1.6, --arg count 3 produces {"count":"3"}, while --argjson count 3 produces {"count":3}. The jq manual documents both options.
Combine a file with generated fields
jq --arg request_id "$REQUEST_ID" \
'. + {request_id:$request_id}' payload.json |
curl --json @- https://api.example.test/endpoint
Ensure the producer writes only the JSON body to standard output. Diagnostic messages should go to standard error or the pipeline can send invalid JSON.
4. Headers, authentication, and response handling
curl --fail-with-body --silent --show-error \
--json @payload.json \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Idempotency-Key: 7d7f2f1e-2a6e-4f17-a9f8-example' \
-D response.headers \
-o response.json \
https://api.example.test/endpoint
- Use
-Hfor authentication, idempotency keys, correlation IDs, or other API-specific headers. - Use
-D -to print response headers, or save them with-D response.headers. - Use
-o response.jsonto keep the response body separate from logs. --fail-with-bodyreturns a failure status for HTTP 4xx/5xx while retaining the server response body.--silent --show-errorsuppresses the progress meter but still reports transfer errors.
Custom headers can override the headers supplied by --json. Keep the request Content-Type and response Accept meanings distinct.
5. Complete runnable examples in other languages
Python with requests
import requests
payload = {"name": "Ada", "active": True, "count": 3}
response = requests.post(
"https://api.example.test/endpoint",
json=payload,
headers={"Authorization": "Bearer YOUR_TOKEN"},
timeout=30,
)
response.raise_for_status()
print(response.json())
The json= parameter serializes the object and sets the JSON content type. Do not double-encode it with data=json.dumps(payload) unless you also manage headers yourself.
Node.js 18+
const payload = { name: 'Ada', active: true, count: 3 };
const response = await fetch('https://api.example.test/endpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
console.log(await response.json());
6. Choosing the right method
| Need | Recommended command | Watch for |
|---|---|---|
| Small fixed body | --json '…' |
Shell quoting; curl does not validate JSON |
| Existing document | --json @payload.json |
The file must contain valid JSON |
| Pipeline or generated body | --json @- |
Only the intended JSON may reach stdout |
| curl older than 7.82.0 | --data-binary plus JSON header |
--data-binary alone is not JSON labeled |
| Dynamic values | jq --arg/--argjson |
Choose string versus parsed JSON type |
7. Validation and debugging checklist
- Validate a file before sending it:
jq empty payload.json. - Print generated JSON:
jq -n --arg name "$NAME" '{name:$name}'. - Inspect the exact exchange with
--verbose; never include secrets in shared logs. - Confirm the request header: look for
Content-Type: application/json. - Check the HTTP status and response body with
--fail-with-body. - Confirm that a proxy, redirect, or server endpoint has not changed the method or authentication requirements.
8. Common errors and fixes
| Symptom | Cause | Fix |
|---|---|---|
| 415 Unsupported Media Type | Body was sent as form data | Use --json, or add -H 'Content-Type: application/json' with --data-binary. |
| 400 invalid JSON | Broken quoting, trailing comma, or producer output mixed with diagnostics | Run the body through jq empty; use jq variables instead of string concatenation. |
| Numbers arrive as strings | --arg always creates strings |
Use --argjson for already-valid numeric or boolean JSON. |
| Newlines or null bytes disappear | --data normalizes file input |
Use --data-binary for exact bytes. |
--json: unknown option |
curl is older than 7.82.0 | Upgrade curl or use the --data-binary fallback. |
| Authentication appears missing after redirect | Redirect handling can change where credentials are sent | Inspect with --verbose; follow only trusted redirects and configure authentication for the final host. |
| Command hangs | Network, DNS, TLS, or server delay | Set a timeout such as --connect-timeout 10 --max-time 60 and inspect verbose output. |
9. Performance, reliability, and cost notes
- For small payloads, inline JSON and file input have similar transfer costs; jq adds local CPU time for generation.
- Streaming with
@-avoids temporary files and works well for pipelines, but make sure the producer finishes with one complete JSON document. - Use connection and total timeouts so a script cannot wait forever. Retry only operations that are safe to repeat, or send an idempotency key when the API supports it.
- Do not log access tokens or sensitive JSON. Redact request bodies and headers in CI output.
- curl itself has no API usage charge. Your API provider may charge for requests, bandwidth, or processing; check that service’s pricing and retry guidance.
Or skip the browser setup
If your POST workflow ultimately exists to capture a URL for documentation, QA, previews, or automation, ScreenshotNeo returns a screenshot or PDF from one API request. The endpoint is a GET request, so no browser installation or JSON POST body is required:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Equivalent clients are shown below. See the ScreenshotNeo API documentation for the complete option list.
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners, newsletter popups, and chat widgets are removed before the shot.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed; response headers identify the page verdict and billing result.
- An MCP server lets Claude, Cursor, and other MCP clients use
take_screenshot,get_page_info, andcapture_pdf. - Free accounts include 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account to get started.
FAQ
Does curl validate JSON when I use --json?
No. curl sets headers and transfers the bytes; validate or generate the document with jq or another JSON parser.
Can I use --json with a JSON array?
Yes. Any valid JSON value, including an array, object, string, number, boolean, or null, can be sent as the body.
When should I use --data-raw?
It is useful when you need data semantics without treating a leading @ as a filename, but it still does not label the body as JSON. Add the content type yourself.
Is Accept required?
No. It expresses the response format you prefer. The server may return JSON regardless, and --json supplies the common JSON preference automatically.
How do I send a JSON file without changing line endings?
Use --data-binary @file.json with an explicit JSON content type, or --json @file.json on curl 7.82.0 and newer.


