Skip to content

How to Send a POST Request with curl (With JSON Body Examples)

How to send a POST request with curl — JSON bodies, form data, file uploads, and the Content-Type header curl doesn't set for you automatically.

Try it now: cURL to JavaScript Fetch Converter Paste a cURL command and get clean fetch() code with headers, body, auth and method preserved. Nothing is sent to a server.

Step 1: The Basic curl POST Request

The flag that turns any curl command into a curl post request is -X POST, placed before the URL:

basic POST
curl -X POST https://api.example.com/users

On its own that sends an empty POST body — useful for endpoints that trigger an action rather than accept data, like /logout or /refresh. Worth knowing early: -X POST is technically implied the moment you add -d or --data, so curl -d '{}' https://api.example.com/users also sends a POST without the flag spelled out. Writing -X POST explicitly is still the better habit — a command is easier to scan when the method is visible rather than inferred — but there is one gotcha worth flagging: if the request hits a redirect (a 301/302/307/308) and you passed -X POST explicitly, older curl versions and some servers will replay that literal method on the redirected request in a way that differs from what curl infers automatically from -dalone. It rarely bites, but it's the kind of inconsistency that's worth remembering the one time it does.

Step 2: Sending a JSON Body

Sending a curl post json body means two flags working together, not one:

curl POST with a JSON body
curl -X POST https://api.example.com/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Ada","role":"admin"}'

The -H "Content-Type: application/json" header is not optional decoration — curl does not set it for you. Left off, curl defaults to Content-Type: application/x-www-form-urlencoded for any body sent with -d, regardless of what that body actually contains. This is the single most common curl post with headers bug: the JSON you typed is completely valid, the request goes through with a 200 or a confusing 400, and the body arrives at the server labeled as form data instead of JSON. Frameworks that pick their body parser based on Content-Type — which is most of them — either silently fail to populate req.bodyor throw a parsing error that has nothing to do with your JSON being malformed. If a POST "isn't working" and the JSON looks fine on inspection, the header is the first thing to check.

Step 3: Sending Form Data Instead

If the API actually expects application/x-www-form-urlencoded — classic HTML form submission format — skip the JSON header entirely and just pass key-value pairs:

form-encoded POST, no extra header needed
curl -X POST https://api.example.com/login \
  -d "name=Ada&role=admin"

This is curl's default behavior with -d, which is exactly why step 2's header matters so much — the default is form encoding, not JSON. The catch with typing values by hand this way is that anything containing a space, an ampersand, or another character with special meaning in a query string gets corrupted or truncated unless it's percent-encoded first. That's what --data-urlencode is for:

--data-urlencode handles the special characters for you
# "name=Ada Lovelace" — plain -d would break on the space
curl -X POST https://api.example.com/users \
  --data-urlencode "name=Ada Lovelace" \
  --data-urlencode "bio=math & computing"

Each --data-urlencode value is percent-encoded on the way out — the space becomes %20, the ampersand inside bio becomes %26— so it can't be mistaken for a field separator by the server. Reach for it any time a value comes from something a user typed rather than a literal you control.

Step 4: Uploading Files with Multipart Form Data

For file uploads, switch from -d to -F:

multipart/form-data upload
curl -X POST https://api.example.com/upload \
  -F "[email protected]" \
  -F "name=Ada"

-F builds a multipart/form-data body and sets the Content-Typeheader itself, including the boundary string the server needs to split fields apart — that's not something you should ever set by hand. The @ prefix on [email protected] tells curl to read that argument as a file path and attach its contents, rather than sending the literal string @photo.jpg.

One thing that trips people up: -F and -ddon't combine the way it seems like they should. Each is building a completely different body format and setting a different Content-Type, so mixing them in one command doesn't merge a JSON field into a multipart upload — it just means the last flag processed usually wins, or curl errors out. If a field needs to travel alongside a file, add it as another -F, not a -d.

Step 5: Adding Authentication Headers

Most real APIs require a token, and that's just another -H flag, stacked alongside the JSON content type:

curl POST with a bearer token
curl -X POST https://api.example.com/orders \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{"item":"widget","qty":2}'

Repeat -H for every header the request needs — an API key header, a custom X-Request-Id, an Acceptoverride — curl doesn't limit how many you pass.

Step 6: Reading the Body from a File

Once a JSON payload gets past a handful of fields, typing it inline gets unwieldy and error-prone — a missing quote in a long inline string is hard to spot. Point -d at a file instead by prefixing the path with @:

reading the request body from a file
curl -X POST https://api.example.com/users \
  -H "Content-Type: application/json" \
  -d @body.json

curl reads body.json from disk and sends its exact contents as the body, which also means the payload can be edited, diffed, and version-controlled like any other file — a real improvement over a growing wall of escaped quotes on one line.

Step 7: From a Working curl Command to Real Code

Once a curl post request is working — the right headers, the right body format, auth passing — the next step is usually turning it into code inside the actual application: a fetch call in a frontend, a requestscall in a backend script, whatever the stack happens to be. Hand-translating every flag is exactly the kind of mechanical, error-prone step a tool should do instead of a person — it's easy to drop a header or misquote a body while retyping it into a different language's syntax. cURL to JavaScript Fetch Converter takes the exact curl command from this walkthrough and turns it into clean fetch() code with headers, body and method preserved — nothing sent to a server in the process. The same conversion exists for Python's requests library and, for Axios and Node's built-in https module, the full set of curl-to-code converters.

Frequently asked questions

How do I send a POST request with curl?

Use curl -X POST followed by the URL, and add a body with -d if the request needs one: curl -X POST https://api.example.com/users -d '{"name":"Ada"}'. If the body is JSON, also add -H "Content-Type: application/json", since curl does not set that header automatically.

How do I send curl post json without curl mangling it?

Pass the JSON as a single-quoted string to -d and add -H "Content-Type: application/json" explicitly. Without that header, curl defaults to application/x-www-form-urlencoded, which is the most common reason a perfectly valid JSON body still gets rejected or misread by the server.

Is curl -X post always required, or does -d imply it?

-d (or --data) implies POST on its own, so -X POST is technically redundant when a body is present. It's still worth typing explicitly for readability, though it's worth knowing that some curl versions and servers handle an explicit -X POST differently on a redirected request than the implied method would be handled.

How do I send curl post with headers, like an auth token?

Stack as many -H flags as the request needs: curl -X POST https://api.example.com/orders -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{...}'. Order between -H flags doesn't matter to curl.

How do I send form data instead of JSON with curl?

Pass key=value pairs directly to -d, like -d "name=Ada&role=admin" — that's curl's default body format, application/x-www-form-urlencoded, so no extra header is needed. If a value contains a space, an ampersand, or another special character, use --data-urlencode instead so curl percent-encodes it correctly.

Why doesn't mixing -F and -d in the same curl command work?

-F builds a multipart/form-data body with its own boundary and sets Content-Type itself, while -d builds a completely different body format. They aren't designed to merge into one request. If a field needs to travel alongside an uploaded file, add it as another -F rather than a -d.

How do I send a large JSON body without typing it inline?

Save the payload to a file and point -d at it with an @ prefix: -d @body.json. curl reads the file's exact contents as the request body, which also makes the payload easy to edit, diff and version-control.

Last updated