curl JSON Examples: POST, PUT, PATCH, Authentication, Pretty Print, and File Upload

Last updated:

curl is the default tool for poking JSON APIs from the terminal — every backend engineer ends up writing variations of curl -X POST -H 'Content-Type: application/json' -d '...' URL dozens of times a week. This page collects the recipes that actually come up: POST with an inline body and from a file, GET with query parameters and an Accept header, PUT and PATCH for updates, Bearer and Basic auth, multipart uploads that mix a file with JSON metadata, pretty-printing with jq, and the newer --json shortcut that landed in curl 7.82. Each example is copy-paste-ready against curl 8.x — the current series in 2026 — and notes the shell pitfalls (single vs double quotes, Windows cmd quirks,@file vs --data-binary) that trip people up.

Got a curl response that errors out as invalid JSON? Paste the body into Jsonic's JSON Validator — it flags the exact line and column where parsing breaks so you can tell the server bug from the curl bug.

Validate JSON response

POST JSON: -d, --data-raw, -H Content-Type

The canonical POST-JSON command has three pieces — a method, a content-type header, and a body. curl is forgiving enough that you can drop -X POST when -d is present (curl flips to POST automatically as soon as it sees a body), but writing the method explicitly makes the command self-documenting and easier to grep through history.

# Inline JSON body — single quotes protect $ and backticks from the shell
curl -X POST \
  -H 'Content-Type: application/json' \
  -d '{"name":"Ada Lovelace","role":"engineer"}' \
  https://api.example.com/users

The single quotes around the JSON matter. With double quotes the shell would expand $variables, backticks, and history references inside the body. If you need a shell variable inside the body (a dynamic ID, a timestamp), build the body in a separate variable first or read it from a file:

# Body from a file — @filename triggers file read
curl -X POST \
  -H 'Content-Type: application/json' \
  -d @user.json \
  https://api.example.com/users

# Same thing, byte-exact (recommended for files)
curl -X POST \
  -H 'Content-Type: application/json' \
  --data-binary @user.json \
  https://api.example.com/users

The difference between -d and --data-binary shows up with multi-line JSON files: -d strips newlines (a holdover from form-data behavior), --data-binary preserves them. For most JSON endpoints either works, but byte-exact is the safer default. --data-raw sits in between — it skips the @ file-read interpretation, useful when your inline JSON literally starts with @.

GET with query parameters + Accept: application/json

For a GET request, curl needs only the URL — no method flag, no body. The two additions that show up on JSON APIs are query parameters (URL-encoded after a ?) and an explicit Accept header that asks the server for JSON when the same endpoint can return HTML, XML, or CSV.

# Plain GET with inline query string
curl 'https://api.example.com/users?role=admin&limit=10'

# Same thing but URL-encode safely with --data-urlencode + -G
curl -G \
  --data-urlencode 'q=Ada Lovelace' \
  --data-urlencode 'role=admin' \
  https://api.example.com/users

# Ask for JSON explicitly when the endpoint content-negotiates
curl -H 'Accept: application/json' \
  https://api.example.com/users/42

The -G flag is the safest way to build query strings with values that might contain spaces, &, =, or non-ASCII characters. Without -G, you would have to URL-encode each value by hand — %20 for spaces, %26 for ampersands. With -G, every --data-urlencode argument turns into a properly encoded key=value pair in the query string.

The Accept header tells content-negotiating APIs which representation you want. For dedicated JSON APIs (REST endpoints that only return JSON) the header is optional, but it costs nothing and documents intent — most major frameworks (Express, Rails, Spring) respect it.

PUT and PATCH for updates

PUT and PATCH share the same shape as POST — only the method changes. The semantic split: PUT replaces the entire resource (you send the full new state), PATCH modifies specific fields (you send only what changes). REST APIs typically support both; some only support PATCH for partial updates.

# PUT — full resource replacement
curl -X PUT \
  -H 'Content-Type: application/json' \
  -d '{"id":42,"name":"Ada Lovelace","role":"engineer","active":true}' \
  https://api.example.com/users/42
# PATCH — partial update (only changed fields)
curl -X PATCH \
  -H 'Content-Type: application/json' \
  -d '{"role":"staff-engineer"}' \
  https://api.example.com/users/42

# PATCH with JSON Merge Patch (RFC 7396) — null deletes a field
curl -X PATCH \
  -H 'Content-Type: application/merge-patch+json' \
  -d '{"nickname":null}' \
  https://api.example.com/users/42

Some APIs use a specific content type for patch bodies — application/merge-patch+json for RFC 7396 (null-deletes-field semantics) or application/json-patch+json for RFC 6902 (an explicit ops array). When the server requires one of these, set the header even if the body looks like ordinary JSON; the framework dispatches the parser based on content type.

For PATCH or PUT requests that return the updated resource, pipe the response through jq to verify the change took effect — see the pretty-print section below.

Authentication: Bearer token, Basic auth, custom header

JSON APIs use one of three auth schemes in practice. Bearer tokens (OAuth, JWT, API key) ride in the Authorization header with the Bearer scheme. HTTP Basic auth uses -u username:password, which curl base64-encodes into the same header. Custom headers (X-API-Key, vendor-specific schemes) go through -H.

# Bearer token — store in env var, reference with double quotes
export TOKEN='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
curl -H "Authorization: Bearer $TOKEN" \
  https://api.example.com/me

# HTTP Basic auth — curl base64-encodes user:password
curl -u 'alice:s3cr3t' https://api.example.com/private

# Custom header — vendor-specific API keys
curl -H 'X-API-Key: sk_live_abc123' \
  -H 'X-Vendor-Version: 2026-05' \
  https://api.example.com/data

The quoting matters. Single quotes around the Bearer header would prevent $TOKEN from expanding — curl would send the literal text $TOKEN. Double quotes let the shell substitute the variable but still treat the rest as one argument. Never paste a long token directly into a curl command line — it ends up in ~/.bash_history or ~/.zsh_history forever. Export it once, reference by name.

For Basic auth, -u user: (trailing colon, no password) prompts curl to ask for the password interactively without echoing — handy for one-off calls without storing credentials. For HMAC-signed requests (AWS SigV4, Stripe webhooks), the signature goes through -H too, but the value is computed externally; see the JWT in headers guide for token formatting rules.

Reading JSON from a file with @file syntax

When the JSON body is more than a couple of fields, inline quoting becomes a maintenance problem — every apostrophe, every embedded double quote turns into a shell-escape puzzle. The fix is to write the body to a file and reference it with @filename.

# Write the body to disk
cat > order.json <<'EOF'
{
  "customer": "alice@example.com",
  "items": [
    {"sku": "ABC-001", "qty": 2, "price": 29.99},
    {"sku": "DEF-042", "qty": 1, "price": 9.99}
  ],
  "notes": "Don't gift-wrap"
}
EOF

# POST it — @ tells curl to read from the file
curl -X POST \
  -H 'Content-Type: application/json' \
  --data-binary @order.json \
  https://api.example.com/orders

Use --data-binary @file instead of plain -d @file for JSON — the binary variant preserves newlines and whitespace exactly as they appear on disk. Plain -d strips newlines, which usually does not break the JSON parser but does change the Content-Length and can confuse signature verification middleware that hashes the raw body.

In curl 7.82 and newer, the simplest form is --json @file: it reads the file as the body, sets Content-Type: application/json, sets Accept: application/json, and switches the method to POST — three flags collapsed into one.

# curl 7.82+ shortcut
curl --json @order.json https://api.example.com/orders

The @- form reads the body from stdin, which composes nicely with shell pipelines: jq '.items[0] = {"sku":"NEW"}' order.json | curl --json @- https://api.example.com/orders mutates a field on the fly and POSTs the result.

Pretty-printing JSON responses (jq pipe, --json flag)

Raw JSON arrives as a single long line with no whitespace — the wire format optimizes bytes, not readability. Three tools turn it into something a human can scan: jq (the standard), python3 -m json.tool (always available on Linux and macOS), and built-in curl writeout for trivial inspection.

# Pipe through jq — colorized, indented, with -s to silence curl's progress meter
curl -s https://api.example.com/users | jq

# Extract a specific field with a jq filter
curl -s https://api.example.com/users/42 | jq '.email'

# Filter and reshape — pull just name + email from each user in an array
curl -s https://api.example.com/users | jq '.[] | {name, email}'

# Python fallback when jq is not installed
curl -s https://api.example.com/users | python3 -m json.tool

The -s (silent) flag on curl is required when piping — without it, the progress meter prints to stderr and clutters the terminal. Use -sS (silent but show errors) for scripts where you still want curl errors visible.

For inspecting both headers and body in one call, combine -i (include response headers) with a pretty-printer that ignores non-JSON prefixes — most jq wrappers do not handle the mixed-mode output, so two separate calls are easier: one with -I (headers only) and one piped through jq. The jq cheat sheet covers the most common filters (selectors, map, group_by, paths) in one page.

# curl 8.x has --write-out + --output for structured request summaries
curl -s -o /tmp/body.json \
  -w 'status=%{http_code}\ntime=%{time_total}s\nsize=%{size_download}\n' \
  https://api.example.com/users

Uploading JSON + file (multipart/form-data with metadata)

When an endpoint accepts a file plus structured metadata, the request is multipart/form-data — not application/json. Each part has its own content type. curl builds these requests with the -F (form) flag: one -F per part, with ;type=to override the part's content type when curl cannot infer it.

# Upload a file plus JSON metadata in one request
curl -X POST \
  -F 'file=@photo.jpg;type=image/jpeg' \
  -F 'metadata={"album":"Vacation","tags":["beach","2026"]};type=application/json' \
  https://api.example.com/uploads

The first part sends photo.jpg as a file (the leading @ on the value triggers file mode); the second sends a JSON blob as a named part with explicit application/jsoncontent type so the server's JSON parser picks it up instead of treating it as a plain string field.

For bigger files, add --progress-bar to replace the noisy default progress meter with a clean single-line bar:

curl -X POST \
  --progress-bar \
  -F 'video=@trip.mp4;type=video/mp4' \
  -F 'metadata={"title":"Roadtrip"};type=application/json' \
  -H "Authorization: Bearer $TOKEN" \
  https://api.example.com/videos \
  -o response.json

If the API uses a different field name than file, change the key accordingly — -F 'avatar=@me.png' uploads to a field called avatar. For comparison, see the Postman comparison — Postman builds the same multipart request through a UI form with the same field semantics.

Common pitfalls: shell escaping, single quotes, Windows cmd

Most curl-JSON failures are not curl bugs — they are shell quoting bugs. The shell processes the command line before curl sees it, expanding variables, interpreting backticks, and stripping outer quotes. Get the shell layer wrong and curl receives a mangled body it then sends faithfully.

Single vs double quotes (bash, zsh, sh). Single quotes preserve every character literally — nothing inside expands. Double quotes allow $variable, `command`, \n, and history substitutions. For JSON bodies, default to single quotes; switch to double quotes only when you need shell substitution inside the body, and remember to escape any inner double quotes the JSON contains.

# WRONG — double quotes expand $price as a shell variable
curl -d "{\"price\": \"$9.99\"}" URL          # body becomes {"price": ".99"} (!) because $9 is empty

# RIGHT — single quotes pass the body untouched
curl -d '{"price": "$9.99"}' URL                # body is exactly {"price": "$9.99"}

# RIGHT when you need a real variable inside the body
NAME='Ada'
curl -d "{\"name\": \"$NAME\"}" URL          # body becomes {"name": "Ada"}

Apostrophes inside single-quoted JSON. Single quotes inside single quotes need the close-escape-reopen trick: 'O'\''Brien' evaluates to O'Brien. For anything beyond one apostrophe, dump the body to a file and use @file.json.

Windows cmd.exe. cmd does not treat single quotes specially — -d '{"a":1}' sends the literal string including the quotes. On Windows, use double quotes for the shell layer and escape inner double quotes with backslash:

:: Windows cmd.exe — escape inner double quotes with backslash
curl -X POST -H "Content-Type: application/json" ^
  -d "{\"name\": \"Ada\"}" ^
  https://api.example.com/users

PowerShell. PowerShell aliases curl to Invoke-WebRequest by default, which has a different argument syntax entirely. Use curl.exe to force the real binary, or switch to Invoke-RestMethod which handles JSON natively. The reference workflows in Parse JSON in Bash and fetch() in JS sit one layer above the shell layer and avoid most of these escaping problems. For API design choices on the server side that make curl debugging easier, see JSON API design.

Key terms

curl
A command-line tool and library (libcurl) for transferring data with URLs. Ships on macOS, most Linux distros, and modern Windows. Current series is curl 8.x as of 2026.
-d / --data
Sets the request body. URL-encodes form-style content by default; strips newlines from file reads. Implies -X POST unless another method is set. For binary-exact bodies prefer --data-binary.
--json
curl 7.82+ shortcut that sets the body, sets Content-Type: application/json and Accept: application/json, and switches the method to POST — a one-flag replacement for the four-flag JSON POST recipe.
Bearer token
An opaque or JWT credential carried in the Authorization: Bearer <token> header. The OAuth 2.0 standard scheme for API authentication; pair with HTTPS to keep the token out of intermediaries.
@file syntax
A curl convention for reading a flag's value from a file. Used with -d @body.json, --data-binary @body.json, --json @body.json, and -F 'file=@photo.jpg'. The @- form reads from stdin instead of a path.
jq
A command-line JSON processor used to pretty-print, filter, and transform JSON. Pairs with curl through a shell pipe: curl -s URL | jq 'filter'. The default formatter for terminal JSON workflows.
multipart/form-data
An HTTP content type for sending mixed file and field data in one request. curl builds these with one -F per part; each part has its own content type, set with ;type= when curl cannot infer it.

Frequently asked questions

How do I POST JSON with curl?

The shortest correct form is: curl -X POST -H "Content-Type: application/json" -d '{"name":"Ada"}' https://api.example.com/users. Three things matter. First, -X POST sets the method (curl infers POST when -d is present, so you can drop -X if the body is non-empty). Second, the Content-Type header tells the server the body is JSON — without it, many servers reject the request or treat it as form data. Third, the -d flag carries the body. Wrap the JSON in single quotes so the shell does not expand $ or backticks. For curl 7.82 and newer, the --json shortcut handles all three at once: curl --json '{"name":"Ada"}' https://api.example.com/users. It sets POST, adds Content-Type: application/json, sets Accept: application/json, and disables the URL-encoding that plain -d applies to form bodies.

Why do I need -H 'Content-Type: application/json'?

curl defaults the request Content-Type to application/x-www-form-urlencoded when you use -d, because -d was originally designed for HTML form submissions. Most JSON APIs read the Content-Type header to decide how to parse the body — a server expecting JSON will fail or silently ignore form-encoded data. Setting -H "Content-Type: application/json" tells the server explicitly that the body is JSON so it routes to the right parser. Some frameworks (Express with express.json(), FastAPI, Spring Boot) refuse to parse a body without the header, returning 415 Unsupported Media Type or an empty req.body. The header also affects what the server returns in some cases — APIs that content-negotiate based on the request will respond with JSON when they see application/json on the way in. The --json flag in curl 7.82+ adds the header for you.

How do I include a Bearer token in curl?

Add an Authorization header with the Bearer scheme: curl -H "Authorization: Bearer $TOKEN" https://api.example.com/me. Store the token in a shell variable so it does not end up in your shell history when you paste a long string — export TOKEN=eyJhbGc... once, then reference it as $TOKEN. The double quotes around the header are required for $TOKEN to expand; single quotes would send the literal text. For testing, you can also read the token from a file with -H "Authorization: Bearer $(cat token.txt)". Avoid the -u flag for Bearer tokens — -u is for HTTP Basic auth and adds a username:password pair, base64-encoded. For OAuth-style tokens, only the Authorization header with Bearer is correct. See our JWT in headers guide for token formatting details.

How do I read JSON from a file with curl?

Prefix the filename with @ in the -d argument: curl -X POST -H "Content-Type: application/json" -d @body.json https://api.example.com/users. The @ tells curl to read the body from the file instead of using the argument as literal text. This avoids shell quoting headaches for JSON with apostrophes, special characters, or many lines. For binary-safe reads that do not strip newlines, use --data-binary @file.json instead of -d. The plain -d flag strips newlines from file content (a legacy behavior for form bodies); --data-binary leaves the file untouched, which matters for JSON streams or when a server validates the exact byte count. With the --json flag (curl 7.82+), --json @file.json works the same way and adds the JSON headers automatically.

How do I pretty-print curl JSON responses?

The standard pattern is to pipe through jq: curl -s https://api.example.com/users | jq. The -s flag silences curl progress meters so they do not corrupt the JSON stream; jq with no arguments pretty-prints input with colors when the output is a terminal. For specific fields, jq accepts a filter: jq .users[0].email returns just the first user email. If jq is not installed, Python works as a fallback: curl -s url | python3 -m json.tool. For raw text without color codes (useful when piping to another tool), use jq -M. To keep both the headers and the pretty body, run curl with -i and then pipe only the body half — easiest is two calls, one with -I for headers and one without. See our jq cheat sheet for the most common filters.

How do I send special characters in JSON via curl?

Use single quotes around the entire JSON body so the shell does not interpret $, backticks, or backslashes: curl -d '{"text":"Price: $9.99"}'. Inside single quotes, only single quotes themselves need escaping — close the quotes, escape the apostrophe, reopen: '{"name":"O'\''Brien"}'. For anything with many quotes or apostrophes, prefer reading from a file: -d @body.json. JSON strings themselves require backslash escapes for double quotes (\\"), backslashes (\\\\), newlines (\\n), and the control characters — but those go inside the JSON, not the shell layer. On Windows cmd, single quotes do not work; use double quotes for the shell layer and escape inner double quotes with \\". On PowerShell, prefer Invoke-RestMethod instead of curl, which handles JSON natively without quote acrobatics.

What's the difference between -d, --data-raw, and --data-binary?

All three set the request body, but they differ in what curl does to the content. -d (alias --data) URL-encodes the body and strips newlines — designed for HTML form submissions. If you pass JSON containing a + or & via -d, curl mangles it. --data-raw sends the exact argument with no special handling for @ (so @body.json is sent literally, not read as a file) — useful when your JSON starts with @. --data-binary sends bytes exactly as given, preserving newlines and whitespace; this is the right choice for JSON read from a file when byte-exact transmission matters. Practical guidance: for inline JSON, -d is fine in most cases (curl does not URL-encode unless the body looks form-like); for JSON from files, prefer --data-binary @file or the curl 7.82+ --json @file which handles everything correctly.

How do I see the raw HTTP request curl makes?

Add -v (verbose) to print the full request and response, including headers: curl -v -X POST -H "Content-Type: application/json" -d '{"a":1}' https://api.example.com. Lines starting with > are the request curl sends; < are the response from the server; * are curl diagnostic notes. For just the response headers without body, use -I (HEAD request) or -i (include headers with body). For more detail than -v, --trace-ascii dumps every byte to a file in a hex+ASCII format that is helpful when debugging encoding issues. To capture the exact request body curl sends, --trace-ascii - prints to stdout. When debugging TLS or connection problems, --verbose plus --trace-time adds timestamps to every line so you can see where the latency goes.

Further reading and primary sources