ScreenshotNeo

BlogHow-to

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.

By the ScreenshotNeo team1 October 20267 min read

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/json tells the server how to parse the request body.
  • Accept: application/json asks for a JSON response; it does not describe the request body.
  • --data defaults to application/x-www-form-urlencoded, so it is not automatically JSON.
  • --data-binary preserves 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 -H for 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.json to keep the response body separate from logs.
  • --fail-with-body returns a failure status for HTTP 4xx/5xx while retaining the server response body.
  • --silent --show-error suppresses 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

  1. Validate a file before sending it: jq empty payload.json.
  2. Print generated JSON: jq -n --arg name "$NAME" '{name:$name}'.
  3. Inspect the exact exchange with --verbose; never include secrets in shared logs.
  4. Confirm the request header: look for Content-Type: application/json.
  5. Check the HTTP status and response body with --fail-with-body.
  6. 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, and capture_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.