Back to blog
curl post examplecurl tutorialREST APIJSON POSTdeveloper tools

Curl Post Example: A Practical REST API Guide

OutrankSeptember 1, 202613 min read
TL;DR
Practical curl post example guide with runnable JSON, form-data, and file upload snippets, plus auth, headers, debugging, and cross-platform tips for REST APIs.
Curl Post Example: A Practical REST API Guide

You paste a curl POST example from an API README, replace the placeholder token, press Enter, and get a 400, 415, or 401. The response body is empty, the endpoint documentation assumes more HTTP knowledge than it explains, and the useful clue is buried somewhere between shell quoting, headers, and redirects.

A dependable POST request isn't just a URL and a payload. The body format, Content-Type, authentication header, shell, redirect behavior, and diagnostic output all affect what the server receives. The patterns below are designed for real REST API work, where a request must remain understandable in a script, CI job, or production troubleshooting session.

Table of Contents

Why Your First Curl POST Snippet Usually Fails

The first failure usually looks harmless. A developer copies a JSON request, adds an API key, and receives 415 Unsupported Media Type. Another sends a valid token and sees 401 Unauthorized. Someone uploading a file gets a malformed request because the shell expanded or consumed part of the command before curl could process it.

HTTP POST was formally defined in RFC 2616 as a method for submitting an enclosed entity to an origin server, including form submission, bulletin-board posting, and append-style database operations. That history explains why the payload shape and media type matter. A server doesn't interpret JSON, URL-encoded form data, and multipart content as interchangeable bodies.

An infographic illustrating common reasons for curl POST request failures and providing best practices for successful API integration.

Match the symptom to the request defect

  • 415 Unsupported Media Type usually means the body format and Content-Type disagree. Sending JSON without Content-Type: application/json is a common cause.
  • 401 Unauthorized often means the token is absent, expired, malformed, or placed in a custom header the API doesn't recognize. Bearer authentication belongs in Authorization: Bearer TOKEN unless the API explicitly documents another scheme.
  • A 400 Bad Request after adding a file can mean the shell interpreted the file reference or the request used URL-encoded data where the endpoint expects multipart form data.
  • A route mismatch can come from a trailing slash difference. Some frameworks redirect /posts to /posts/, and a redirect can change how the client resends the request. curl documents --post301 and --post302 for preserving POST across those responses, rather than accepting browser-like method changes.

Endpoint terminology also matters when reading documentation. A concise explanation of what API endpoints mean can prevent a basic path or method mismatch before you start debugging payloads.

Practical rule: Treat the method, URL, headers, and body as one contract. Changing only the body rarely fixes a request whose headers describe something else.

The JSON POST Example You Will Actually Reuse

For a JSON API, start with one explicit, readable request. This example uses Captapi's chat completions endpoint as a concrete shape, but the same structure applies to most REST services that accept JSON:

curl -X POST "https://api.captapi.com/v1/chat/completions" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "model": "gpt-4o-mini",
    "messages": [
      {
        "role": "user",
        "content": "Summarize this API response"
      }
    ]
  }'

The -X POST flag makes the method obvious to a reader. In practice, -d or --data already switches the request to POST, so -X POST is usually redundant. I still keep it in onboarding examples when method visibility matters, then remove it in compact scripts once the request contract is clear.

The Content-Type header tells the server to parse the body as JSON. The Authorization header carries the bearer token. The -d argument supplies the payload, and single quotes protect JSON braces and double quotes from Bash or zsh interpretation.

A successful response commonly has an object identifier, a choices collection, and usage information. The exact fields depend on the API, so treat the response schema as authoritative rather than assuming every service returns the same shape.

Why quoting changes the result

On Linux and macOS shells, single-quoted JSON is usually the least troublesome inline form. If you use outer double quotes instead, every JSON quote needs escaping:

curl -X POST "https://api.captapi.com/v1/chat/completions" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d "{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"user\",\"content\":\"Summarize this API response\"}]}"

For a related example of passing curl requests through application code, see this practical guide to using curl with PHP.

Flag Purpose Failure it prevents
-X POST Makes the HTTP method explicit Confusion when a reader expects a POST
-H "Content-Type: application/json" Declares the body format 415 responses and incorrect parsing
-H "Authorization: Bearer YOUR_API_KEY" Sends bearer authentication 401 responses caused by a missing token
-d '{...}' Sends the JSON request body Empty or incorrectly shaped submissions
Endpoint URL Selects the target resource Posting to the wrong route or version

A useful authentication test is to remove the Authorization header intentionally. If the endpoint responds with 401, the route is reachable and the server is enforcing authentication. Add the header again, then inspect the authenticated response. This test separates a credential problem from a payload or routing problem.

Form Data and File Uploads With -F and --data-binary

Not every POST body is JSON. Traditional forms use application/x-www-form-urlencoded, where fields are represented as encoded name and value pairs. curl's tutorial explains that -d sends this form style by default, with spaces converted to + and reserved characters percent-encoded. For values containing punctuation, --data-urlencode is safer than manually assembling the string.

curl "https://api.example.com/login" \
  --data-urlencode "email=developer@example.com" \
  --data-urlencode "password=correct horse battery staple"

Each --data-urlencode option handles one field. This avoids mistakes with spaces, ampersands, question marks, and other characters that have meaning inside a URL-encoded body.

Use -F when the endpoint expects multipart/form-data. Multipart is the right choice for a file upload, a request that combines text fields with binary content, or repeated fields that need separate parts.

curl -X POST "https://api.example.com/v1/files" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@./reports/summary.pdf" \
  -F "description=Quarterly summary"

curl builds the multipart boundary and content disposition for you. Don't manually set the multipart Content-Type unless the API specifically requires it, because the boundary generated by curl must match the body. This multipart form data guide is useful when you need to reason about the individual parts rather than treating the upload as a single opaque body.

Sending a saved body without shell interpretation

--data-binary @file reads a file and sends its contents without the transformations associated with ordinary -d handling. It works well for a saved JSON document, a text payload, or a body generated by another tool.

curl -X POST "https://api.example.com/v1/events" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --data-binary "@event.json"

The leading @ means “read from this file.” It doesn't mean upload the file as a multipart field. That distinction is important.

Flag Resulting Content-Type Typical use case
-d application/x-www-form-urlencoded by default Simple form fields
--data-urlencode URL-encoded form data Form values containing reserved characters
-F multipart/form-data Files, binary parts, and mixed fields
--data-binary @file Depends on the explicit header Raw JSON or text loaded from disk

The decision rule is straightforward: use -d for a small form or inline JSON only when you set the JSON content type, --data-urlencode for encoded form fields, -F for multipart, and --data-binary when a saved file should become the request body.

Adding Authentication and Custom Headers Correctly

A production JSON request usually has two separate media headers:

curl -X POST "https://api.captapi.com/v1/posts" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"title":"A practical API test","body":"Created from curl"}'

Content-Type describes what you're sending. Accept describes what you want back. They aren't duplicates. Some APIs tolerate a missing Accept header, while others negotiate response formats or enforce a documented header set.

Bearer authentication uses the Authorization header:

-H "Authorization: Bearer $TOKEN"

An API-key service may instead require a custom header:

-H "x-api-key: $KEY"

Some services accept a key in the query string, but that approach can expose credentials through shell history, proxy logs, browser history, and monitoring systems. Prefer the documented header form when one exists. For a broader comparison of credential patterns, consult this guide to API authentication methods.

Keep secrets out of command history

For local testing, environment variables are less error-prone than repeatedly pasting secrets:

export TOKEN="replace-me"
curl "https://api.captapi.com/v1/posts" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"title":"Test post"}'

A .netrc file can store credentials for services that support curl's user authentication flow, but it must be protected with appropriate filesystem permissions and should never enter source control. A secret manager is the stronger choice for CI and shared environments.

Custom headers also support operational concerns:

-H "Idempotency-Key: request-identifier"
-H "X-Request-Id: trace-identifier"
-H "User-Agent: api-smoke-test"

An idempotency key helps an API recognize a retry of the same logical operation. An X-Request-Id gives your logs a searchable correlation value. A descriptive user agent helps operators identify the client.

Auth scheme curl -H flag Linux/macOS PowerShell
Bearer token Authorization: Bearer TOKEN -H "Authorization: Bearer $TOKEN" -H "Authorization: Bearer $env:TOKEN"
API key header x-api-key: KEY -H "x-api-key: $KEY" -H "x-api-key: $env:KEY"
Basic authentication User and password credentials -u "$USER:$PASSWORD" -u "$env:USER:$env:PASSWORD"

PowerShell needs quoted headers and backtick line continuations. A backslash at the end of a line is not the same continuation mechanism as it is in Bash, so a command copied from Linux can turn into several unrelated arguments.

Cross Platform Quoting on Linux macOS and PowerShell

The HTTP request is portable. The command-line parser isn't. Bash and zsh generally accept the same single-quoted JSON, while PowerShell applies its own rules to variables, quotes, braces, and line continuation.

Bash:

curl -X POST "https://api.example.com/v1/items" \
  -H "Content-Type: application/json" \
  -d '{"name":"sample","enabled":true}'

macOS Terminal normally uses zsh, and the same form works:

curl -X POST "https://api.example.com/v1/items" \
  -H "Content-Type: application/json" \
  -d '{"name":"sample","enabled":true}'

zsh can still surprise you when an inline value contains an exclamation mark, because history expansion may process it depending on the shell configuration. A body file avoids that class of problem.

PowerShell uses the backtick for line continuation:

curl.exe -X POST "https://api.example.com/v1/items" `
  -H "Content-Type: application/json" `
  -d '{"name":"sample","enabled":true}'

Using curl.exe explicitly can help on Windows hosts where curl resolves to a PowerShell command alias rather than the native executable. PowerShell's single-quoted strings are literal, so the JSON above is often convenient. If you need an environment variable inside the body, use a double-quoted here-string and account for interpolation:

$body = @'
{
  "name": "sample",
  "enabled": true
}
'@

curl.exe "https://api.example.com/v1/items" `
  -H "Content-Type: application/json" `
  --data-binary $body

The common Windows failures are unescaped double quotes inside an already double-quoted -d argument, and braces or variable markers being interpreted unexpectedly by the host. A body file is usually the cleanest escape hatch:

curl.exe -X POST "https://api.example.com/v1/items" `
  -H "Content-Type: application/json" `
  --data-binary "@body.json"

A comparison chart showing how to format a curl POST request with JSON on Linux, macOS, and PowerShell.

Git Bash and WSL mostly follow Bash quoting rules. Native Windows Terminal running PowerShell still requires PowerShell syntax, and behavior can vary between older and newer PowerShell hosts. When a request works in WSL but fails in native PowerShell, inspect the command parsing before questioning the API.

Debugging Failed POST Requests With Verbose and Trace Flags

A failed POST becomes easier to diagnose when you separate transport problems from application errors. Start with response headers:

curl -i -X POST "https://api.example.com/v1/items" \
  -H "Content-Type: application/json" \
  -d '{"name":"sample"}'

The -i flag includes response headers in the terminal output. That often reveals the status, content type, redirect location, request identifier, or server-specific error header without exposing every connection detail.

Use -v when you need to see the request and response headers, connection setup, and TLS negotiation:

curl -v -X POST "https://api.example.com/v1/items" \
  -H "Content-Type: application/json" \
  -d '{"name":"sample"}'

Verbose output helps answer practical questions. Did curl connect to the expected host? Did it follow a redirect? Did it send the Authorization and Content-Type headers? Did the server return an error before reading the body?

Escalate only when the request needs it

--trace-ascii - prints a detailed trace to standard output, including the data exchanged during the request. Use it when a proxy, TLS layer, redirect, or byte-level body issue remains unclear.

curl --trace-ascii - -X POST "https://api.example.com/v1/items" \
  -H "Content-Type: application/json" \
  --data-binary "@body.json"

Be careful. Traces can contain credentials and request data, so redact them before sharing logs.

curl's measurement options are useful for scripts and benchmarking. Its documented statistics include time_total, time_starttransfer, time_redirect, size_upload, and num_redirects, which can expose whether a POST spends time connecting, waiting for the first response, following redirects, or uploading a large body. The broader curl statistics reference describes these diagnostics in the context of network troubleshooting.

curl --fail-with-body \
  -o /dev/null \
  -w "\n%{http_code} %{time_total}s %{size_upload} bytes uploaded\n" \
  -X POST "https://api.example.com/v1/items" \
  -H "Content-Type: application/json" \
  -d '{"name":"sample"}'

--fail-with-body preserves the error response while returning a failure status for HTTP errors. Use --fail when you want failure signaling without retaining the response body. The -w format prints status and timing after the request, while -o /dev/null keeps a large successful response out of the terminal.

If a client disconnects while an upstream service is still processing, the resulting behavior can look different from an ordinary application rejection. The explanation of HTTP error 499 provides useful context for separating client-side cancellation from server-side validation failures.

A technical infographic explaining how to debug failed curl POST requests using verbose and trace flags.

Production Checklist and Next Steps for Automation

A curl command becomes production-worthy when it can fail clearly, protect secrets, and leave enough evidence to explain what happened. Use this compact checklist before putting a POST into a smoke test, deployment script, or CI job:

  • Validate the body: Check JSON against the endpoint schema before sending it.
  • Pin the media type: Set Content-Type explicitly for JSON and multipart requests.
  • Load secrets safely: Read tokens from a secret store or protected environment, not from committed files or casual command history.
  • Fail on HTTP errors: Use --fail-with-body when the error payload matters to CI logs.
  • Capture observability fields: Record the status code, total time, upload size, and a request identifier.
  • Make retries safe: Pair retry logic with an idempotency strategy for operations that create or mutate resources.

From there, move payload construction into jq, use xargs for controlled batches, and place the same request inside GitHub Actions or a container init script. Structured logs should capture the endpoint name, status, duration, and correlation identifier without recording bearer tokens or sensitive bodies.

For reusable shell patterns and deployment-oriented snippets, the cloud-native code examples from CloudCops GmbH provide another place to compare implementation approaches. Captapi also documents ready-to-run curl requests for authenticated social data endpoints, including transcript and post-oriented API calls, so the command-line pattern can remain consistent while the payload changes. These practices align with broader REST API best practices, especially around validation, authentication, retries, and predictable error handling.

A production checklist for API automation showing best practices for request validation, security, and CI pipeline integration.


If you're building social data workflows, visit Captapi to use a consistent REST API with ready-to-run curl examples for transcripts, summaries, comments, analytics, and platform-specific post data. Sign up, create an API key, and test one authenticated POST from the command line before wiring it into your application or CI pipeline.