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:
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 -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:
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:
# "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:
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 -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 @:
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