curl POST request examples: JSON, auth, file upload, and debugging flags
Quick answer
💡The minimal curl POST request with a JSON body is: curl -X POST -H "Content-Type: application/json" -d '{"key":"value"}' https://api.example.com/endpoint. Curl 7.82 and later added the --json flag which sets both Content-Type: application/json and Accept: application/json automatically, shortening the command. Always use -i to include response headers and -v for full debugging output including TLS negotiation.
Error symptoms
- ✕
Server receives the request body as an empty string or raw text instead of parsed JSON - ✕
Authorization header is not sent because the wrong header name is used - ✕
curl follows a redirect but loses the POST body, sending a GET to the final URL - ✕
Request hangs indefinitely with no timeout set - ✕
Response body is compressed and unreadable without --compressed flag - ✕
curl exits with error code 7 (Failed to connect) when the URL is plain HTTP behind an HTTPS proxy
Common causes
- •Missing -H "Content-Type: application/json" header, so server parses body as form data
- •Using -d @filename where filename does not exist or the path is incorrect
- •Not reading the -L flag default (disabled), so curl stops at the redirect response
- •Omitting --max-time and --connect-timeout, leaving curl to hang on slow connections
- •Using a custom auth header like Auth: token instead of Authorization: Bearer token
- •Passing raw JSON with double quotes on Windows without escaping, causing shell parsing errors
When it happens
- •Testing a new REST endpoint before writing application code
- •Debugging a 401 or 403 by manually crafting the Authorization header
- •Submitting form data versus JSON body and getting the wrong content type
- •Uploading files to a multipart endpoint
- •Scripting API calls in shell scripts with retries
Examples and fixes
curl, the --json shortcut (7.82+), and piping a file with -d @filename.
POST with JSON body: three approaches
❌ Wrong
# Missing Content-Type — server may not parse as JSON
curl -X POST https://api.example.com/v1/users \
-d '{"name":"Alice","email":"[email protected]"}'
# Wrong auth header name
curl -X POST https://api.example.com/v1/orders \
-H "Auth: YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{"productId":5,"qty":1}'
# No status check in a script — silently fails
RESPONSE=$(curl https://api.example.com/v1/items \
-d '{"name":"widget"}')
echo $RESPONSE✅ Fixed
# Standard curl POST with JSON body
curl -i -X POST https://api.example.com/v1/users \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"name":"Alice","email":"[email protected]"}'
# curl 7.82+ shortcut: --json sets both Content-Type and Accept automatically
curl -i --json '{"name":"Alice","email":"[email protected]"}' \
https://api.example.com/v1/users
# Read JSON body from a file (useful for large payloads)
curl -i -X POST https://api.example.com/v1/users \
-H "Content-Type: application/json" \
-d @user.json
# Correct Bearer auth header
curl -i -X POST https://api.example.com/v1/orders \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{"productId":5,"qty":1}'
# Script usage: capture HTTP status separately from body
HTTP_STATUS=$(curl -s -o /tmp/response.json -w "%{http_code}" \
-X POST https://api.example.com/v1/items \
-H "Content-Type: application/json" \
-d '{"name":"widget"}')
if [ "$HTTP_STATUS" -ne 201 ]; then
echo "Failed: $HTTP_STATUS"
cat /tmp/response.json
exit 1
fiThe broken examples omit Content-Type, which causes many servers to reject the body or parse it incorrectly as application/x-www-form-urlencoded. The Auth header name is custom and not recognized by standard auth middleware. In the script, the response body is printed but the exit code is ignored, so a 422 or 500 looks identical to a 201. The fixed version uses -i to include response headers in the output, making status codes visible without extra flags. The --json shortcut in curl 7.82 reduces boilerplate. The -w "%{http_code}" write-out format captures the status code separately from the body, enabling reliable success checking in shell scripts.
Multipart form upload, automatic retries, cookie sessions, and jq to extract JSON fields.
File upload, retries, and jq parsing
❌ Wrong
# Sending file content as JSON string instead of multipart
curl -X POST https://api.example.com/upload \
-H "Content-Type: application/json" \
-d '{"file":"<binary content here>"}'
# Retry loop without delay — hammers the server
for i in 1 2 3; do
curl -X POST https://api.example.com/v1/job \
-d '{"task":"process"}'
done
# No timeout — curl hangs indefinitely on a slow endpoint
curl -X POST https://api.example.com/v1/heavy-task \
-d '{"data":"large"}'✅ Fixed
# Multipart file upload with -F flag
curl -i -X POST https://api.example.com/upload \
-F "[email protected]" \
-F "caption=Profile photo" \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
# curl sets Content-Type: multipart/form-data automatically with -F
# Automatic retry with --retry and --retry-delay
curl -i -X POST https://api.example.com/v1/job \
-H "Content-Type: application/json" \
-d '{"task":"process"}' \
--retry 3 \
--retry-delay 2 \
--retry-all-errors
# Timeout flags: connection phase and total request
curl -i -X POST https://api.example.com/v1/heavy-task \
-H "Content-Type: application/json" \
-d '{"data":"large"}' \
--connect-timeout 10 \
--max-time 30
# Cookie session: save on login, send on subsequent requests
curl -X POST https://api.example.com/login \
-c cookies.txt \
-d 'username=alice&password=YOUR_PASSWORD_HERE'
curl -i https://api.example.com/dashboard \
-b cookies.txt
# jq to extract a field from the response
curl -s https://api.example.com/v1/users \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
| jq '.[] | .email'Sending binary file content inside a JSON string body does not work because binary bytes are not valid JSON. Multipart form upload with the -F flag is the correct mechanism: curl sets Content-Type: multipart/form-data with a generated boundary automatically. The retry loop without delay can trigger rate limits or make a server-side issue worse by hitting the endpoint repeatedly. The --retry 3 --retry-delay 2 flags retry on network errors with a 2-second pause, and --retry-all-errors extends retry to HTTP 4xx and 5xx responses. The --connect-timeout flag limits the TCP connection phase while --max-time limits the entire request including data transfer, preventing curl from hanging indefinitely on slow endpoints.
How curl sends data and why it matters
curl is a command-line tool for transferring data using URL syntax, supporting HTTP, HTTPS, FTP, and dozens of other protocols. For API testing and scripting, its HTTP capabilities are most relevant. Understanding how curl constructs a request body is essential for getting the server to receive exactly what you intend.
The -d flag attaches a request body and automatically sets the HTTP method to POST. It accepts three input forms. A plain string like -d '{"key":"value"}' sends the string as the body. A string prefixed with @ like -d @payload.json reads the file content as the body. The special form -d @- reads from standard input, which is useful for piping data into curl.
Critically, -d does not set a Content-Type header automatically. Without Content-Type: application/json, many servers parse the body as application/x-www-form-urlencoded, which is the browser form submission format. A JSON body parsed as form data produces a request with one field whose name is the entire JSON string, which is not what the server expects. Always set -H "Content-Type: application/json" explicitly when sending JSON.
Curl 7.82 introduced the --json flag, which is a shortcut for -H "Content-Type: application/json" -H "Accept: application/json" -d. This covers the most common API testing pattern in a single flag. Check your curl version with curl --version before relying on it, as versions before 7.82 will reject the flag as unknown.
The -F flag is for multipart/form-data, which is used for file uploads and HTML form submissions. Each -F field adds a form part. The value @filename uploads a file from disk. Curl sets the Content-Type header to multipart/form-data with a generated boundary string automatically when -F is used. Do not combine -d and -F in the same command; they produce different content types.
For authentication, the standard header is Authorization: Bearer YOUR_TOKEN_HERE. The Authorization header name is case-insensitive in HTTP but curl is case-sensitive in its own -H parsing. Custom header names like Auth, Token, or X-Auth-Token are not recognized by standard server middleware unless the middleware is explicitly configured for them.
Reading curl output to debug failed requests
Three curl flags provide progressively more diagnostic information. -i includes the response headers before the body in the output. -v includes the full conversation: TLS negotiation details, request headers sent, redirect history, and response headers. -s suppresses the progress meter and is useful in scripts where you want only the response body without curl status output.
The most useful debugging pattern is combining -i with a specific endpoint to see what the server actually received and returned. If the server returns 415 Unsupported Media Type, the Content-Type header is missing or incorrect. If the server returns 401 Unauthorized, the Authorization header is missing or the token is malformed. If the server returns 400 Bad Request, the body is malformed or missing required fields.
For redirect debugging, -v shows each redirect step. Look for a 301 or 302 response followed by the Location header. Notice whether the redirect changes the scheme from http:// to https://, which causes curl to drop the Authorization header by default. Use --location-trusted to preserve headers through redirects only when you trust the redirect target completely.
For TLS issues, -v shows the TLS handshake and the certificate chain. Look for lines starting with SSL connection using to see the negotiated TLS version and cipher suite. Lines starting with Server certificate show the certificate subject and validity dates. If the handshake fails, the error message at the end of the TLS section explains why.
The -w write-out flag extracts specific values from the completed request into a format string. The %{http_code} variable outputs the HTTP status code. The %{time_total} variable outputs the total request duration. The %{size_download} variable outputs the response body size in bytes. Using -o /dev/null -w "%{http_code} %{time_total}" sends the response body to /dev/null and prints only the status code and timing, which is useful for load testing without saving responses.
For testing endpoint behavior before writing application code, /tools/http-request-builder provides a visual interface that constructs the same request that curl would send, making it easy to compare the curl command output with a GUI equivalent.
Correcting common curl command mistakes
The most frequent mistake is sending a JSON body without Content-Type: application/json. The fix is a single -H flag: curl -X POST -H "Content-Type: application/json" -d '{...}' URL. For curl 7.82 and later, --json replaces both the header and -d flags with a single argument.
For redirects that drop the POST body, curl's default behavior on a 301 or 302 is to follow the redirect using GET instead of repeating the POST. This is correct HTTP behavior: redirect responses expect the client to resubmit the request to the new location, and browsers always use GET for simplicity. For API calls, avoid redirects by using the final HTTPS URL directly. If you cannot avoid the redirect, -X POST combined with -L forces curl to repeat the POST, but this violates the redirect semantics and may cause duplicate processing.
For retries in shell scripts, --retry N retries on transient network errors up to N times. --retry-delay SECONDS sets the pause between attempts. --retry-all-errors extends retry behavior to HTTP 4xx and 5xx responses, which is appropriate for some idempotent operations but must be used carefully for non-idempotent POST requests that might cause duplicate side effects.
For timeouts, always set both --connect-timeout and --max-time. --connect-timeout applies only to the TCP connection phase. --max-time applies to the entire request including data transfer. Without --max-time, curl can wait indefinitely for a slow server response. In scripts, 10 seconds for connect-timeout and 30 seconds for max-time are reasonable defaults for most APIs.
For Basic authentication, -u username:password is a shortcut that sets the Authorization: Basic header with the Base64-encoded credentials. For Bearer token auth, the shortcut does not exist; you must set the header explicitly with -H "Authorization: Bearer YOUR_TOKEN_HERE".
For compressed responses, --compressed tells curl to send Accept-Encoding: gzip and decompress the response automatically before printing it. Without this flag, a compressed response body appears as binary garbage in the terminal.
Windows shells, large payloads, and stdin piping
JSON quoting behaves differently between Bash and Windows Command Prompt. In Bash, single quotes protect the JSON string from shell expansion: -d '{"key":"value"}' works correctly. In Windows Command Prompt, single quotes are literal characters, not string delimiters. Use double quotes for the outer shell and escape inner double quotes: -d "{\"key\":\"value\"}". PowerShell has its own quoting rules that differ from both.
For large request bodies, avoid putting the JSON directly in the command because it hits shell argument length limits and is hard to read. Write the payload to a file and read it with -d @payload.json. The @ prefix tells curl to read the file content rather than treating the string as the body. The file must exist and be readable; if it does not exist, curl exits with exit code 26 (Read error) with no other indication of the problem.
Stdin piping with -d @- allows other tools to generate the request body dynamically. For example: echo '{"key":"value"}' | curl -X POST -H "Content-Type: application/json" -d @- URL. This is useful when the body is generated by a previous command in a pipeline. The drawback is that curl reads stdin to completion before sending, so this pattern does not work for streaming body generation.
Session cookies require two flags used consistently: -c cookies.txt saves cookies from responses, and -b cookies.txt sends saved cookies with requests. Use both together for endpoints that require a session-based authentication flow: first POST to the login endpoint with -c, then GET or POST authenticated endpoints with -b. The cookie file is a plain text file in Netscape format that curl reads and writes automatically.
On macOS, the system curl may be older than 7.82 and lack the --json flag. Install a newer version with Homebrew: brew install curl. The Homebrew version installs to a different path and may require adding it to PATH before the system curl. Run which curl and curl --version to verify which version is active.
When piping curl output to jq, use -s to suppress the progress meter so that jq receives only the response body. Without -s, jq attempts to parse the progress meter output as JSON and fails.
Flags that produce misleading results
Using -k or --insecure disables TLS certificate verification. The request appears to succeed because curl stops checking the certificate, but the connection provides no protection against man-in-the-middle attacks. This flag is sometimes used to silence certificate errors in development and then left in place in staging scripts. Always fix the underlying certificate issue rather than disabling verification. In local development, use mkcert to create trusted certificates.
Using -X GET with -d sends a GET request with a request body. Most web servers ignore bodies on GET requests. Some servers, API gateways, and proxies reject GET requests with bodies outright. For data retrieval, use query parameters instead of a request body: curl "https://api.example.com/v1/users?status=active" rather than a GET with a JSON body.
The -L flag follows redirects, which is often what you want. However, -L on a POST request converts the method to GET when following a 301 or 302 redirect. If the redirected endpoint expects a POST, the server returns 404 or 405 Method Not Allowed. For POST-specific redirects, use 307 Temporary Redirect on the server side, which preserves the POST method through the redirect.
Omitting -i means response headers are not printed. When debugging an authentication failure, the response headers often contain the WWW-Authenticate header that explains exactly which authentication scheme is required. Without -i, you see only the 401 body, which may be less informative.
Using --data-raw instead of -d has a specific meaning. --data-raw sends the string exactly as given without any interpretation of @ prefixes or newline stripping. -d strips newlines from the data and reads files when the string starts with @. Use --data-raw when you want a literal @ character in the body or when the body contains newlines that must be preserved.
Not following security guidance for secrets in shell commands is a risk in multi-user environments. Shell commands with secrets in the -d flag or -H flag are visible in shell history and in process listings that other users can read with ps aux. Use environment variables to store tokens and reference them with $TOKEN in curl commands, or read secrets from a file.
Scripting curl for reliable API automation
Build reliable shell scripts around curl by checking exit codes and HTTP status separately. curl's exit code reflects network and protocol errors, not HTTP status codes. A successful connection to a server that returns 500 exits with code 0. Capture the HTTP status using -w "%{http_code}" and -o to redirect the body to a file: HTTP_STATUS=$(curl -s -o /tmp/response.json -w "%{http_code}" ...). Then check both the exit code with $? and the HTTP status with the captured variable.
Always set timeouts in scripts. Unattended scripts that hang on slow API responses block CI pipelines, scheduled jobs, and deployment scripts indefinitely. Use --connect-timeout 10 --max-time 30 as a baseline and adjust based on the expected response time of each endpoint.
Store secrets in environment variables rather than embedding them in curl commands. Reference them as -H "Authorization: Bearer $API_TOKEN" rather than pasting the token directly. This keeps secrets out of shell history, process listings, and version control. In CI systems, inject secrets as environment variables from a secrets manager.
Use jq to parse and transform JSON responses in shell scripts. curl -s URL | jq '.users[] | select(.active == true) | .email' extracts a filtered list of fields without writing a Python or Node.js script. jq is available in most package managers and can be used with -c for compact output or -r for raw string output that removes JSON string quotes.
For API endpoints that you regularly test, consider using /tools/http-request-builder to build and test the request interactively first, then convert the result to a curl command for scripting. This avoids the iteration cycle of writing a curl command, running it, reading the error, and adjusting flags.
For multipart uploads in scripts, write the file path into the -F flag rather than reading the file content into a variable. curl handles the file reading and boundary generation efficiently. Trying to base64-encode a file and embed it in a JSON body instead of using multipart is a common workaround that works but increases payload size by 33 percent and is less broadly supported by file-handling endpoints.
curl POST request checklist
- ✓Include -H "Content-Type: application/json" for JSON bodies, or use --json on curl 7.82+.
- ✓Use -H "Authorization: Bearer YOUR_TOKEN_HERE" for bearer auth, not a custom header name.
- ✓Add -i to see response headers including status code without switching to -v.
- ✓Set --connect-timeout 10 and --max-time 30 in scripts to prevent hanging.
- ✓Use -d @filename for large payloads instead of embedding JSON in the command.
- ✓Use -F for file uploads instead of base64-encoding binary content in a JSON field.
- ✓Capture HTTP status separately with -w "%{http_code}" -o /tmp/body.json in shell scripts.
- ✓Use -s and pipe to jq when parsing JSON fields from the response.
Related guides
Frequently asked questions
What is the difference between curl -d and curl --json?
The -d flag attaches a request body but does not set Content-Type automatically. --json, added in curl 7.82, sets Content-Type: application/json and Accept: application/json automatically before attaching the body. --json is a shortcut for the most common API testing pattern. Both flags set the HTTP method to POST implicitly, so -X POST is optional when using either.
Why does curl send a GET after following a redirect?
RFC 9110 specifies that clients following a 301 or 302 redirect should use GET for the redirected request even if the original request was POST. This prevents accidental double-submission of form data. For APIs that need the POST to be repeated, the server should respond with 307 Temporary Redirect, which explicitly preserves the HTTP method. Using -X POST with -L forces POST on the redirect but violates the redirect semantics.
How do I send a JSON array instead of an object with curl?
Wrap the array in the -d value: curl -X POST -H "Content-Type: application/json" -d '[{"id":1},{"id":2}]' URL. The Content-Type header identifies the body as JSON regardless of whether the top-level value is an object or an array. For large arrays, write the JSON to a file and use -d @payload.json to avoid shell argument length limits.
How do I handle cookies with curl for a session-based API?
Use -c cookies.txt to save cookies from the response and -b cookies.txt to send saved cookies on subsequent requests. POST to the login endpoint with -c to capture the session cookie. Then send subsequent requests with -b to include the session. The cookie file is in Netscape format and curl manages reading and writing it automatically.
What does curl exit code 7 mean?
Exit code 7 means Failed to connect to host. The TCP connection could not be established. Common causes include the server not running on the specified port, a firewall blocking the connection, a DNS resolution failure, or an incorrect URL scheme. Run curl -v to see the exact connection attempt and identify which step failed.
How do I make curl retry on failure automatically?
Use --retry N to retry up to N times on transient network errors, --retry-delay SECONDS to pause between attempts, and --retry-all-errors to extend retry behavior to HTTP 4xx and 5xx responses. For example: --retry 3 --retry-delay 2 --retry-all-errors. Use --retry-all-errors carefully for POST requests because retrying may cause duplicate processing on non-idempotent endpoints.
How do I pipe curl output to jq and extract a field?
Use -s to suppress the progress meter so jq receives only the JSON body, then pipe to jq with the filter. For example: curl -s https://api.example.com/users -H "Authorization: Bearer YOUR_TOKEN_HERE" | jq '.[] | .email'. The -s flag is critical; without it jq tries to parse the progress output as JSON and fails with a parse error.
What is the --compressed flag and when do I need it?
The --compressed flag tells curl to add Accept-Encoding: gzip to the request and decompress the response automatically. Without it, a server that compresses responses sends a binary compressed body that appears as unreadable characters in the terminal. Add --compressed to any curl command where the server may return compressed content, which includes most production APIs that optimize bandwidth.
How do I capture both the HTTP status code and the response body in a shell script?
Use -o to write the body to a file and -w to write the status code to stdout: HTTP_STATUS=$(curl -s -o /tmp/response.json -w "%{http_code}" -X POST ...). After the command, $HTTP_STATUS contains the numeric status code and /tmp/response.json contains the body. Check if [ "$HTTP_STATUS" -ne 200 ] to detect errors and read the file for the error details.
All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-06.