curl POST: how to send JSON, headers and a token
Short answer: to send JSON with curl, pass it with -d and add a Content-Type: application/json header. You don't need -X POST: a body makes curl send a POST on its own.
curl https://playground.tetiva.app/post \
-H "Content-Type: application/json" \
-d '{"name": "Tetiva", "qty": 2}'
Leave the header out and curl sends the body as form data, which many APIs answer with 400 or 415. On curl 7.82 or later, --json does both in one flag.
You can run that command right now. It points at our test API, which echoes the request back exactly as the server received it.
What the command is made of#
curl sends HTTP requests from the terminal. It ships with macOS and with Windows 10 and 11, and on Linux it's usually installed already or one package away.
The URL. It can go first or last; curl doesn't mind. Quote it if it contains & or ?, otherwise the shell reads & as "run in the background".
-H, a header. Written as "Name: value". Add as many as you need, one -H each.
-d, the body. Everything after it goes to the server as is. Wrap JSON in single quotes so the double quotes inside reach curl untouched. That works on macOS, Linux and Git Bash; Windows has its own rules, covered below.
Do you need -X POST?#
Not with -d. Run curl -v -X POST -d ... and curl says so itself: Note: Unnecessary use of -X or --request, POST is already inferred.
You need -X in two cases:
- POST with no body, such as restarting a job. Without
-X, curl would send a GET. - Other methods with a body:
-X PUT -d '...'or-X PATCH -d '...'.
curl -X POST https://api.example.com/jobs/42/restart
curl -X DELETE https://api.example.com/orders/42
A DELETE, as on the second line, usually needs no body.
A GET request with curl#
curl with nothing but a URL sends a GET and prints the response body. Query parameters go straight into the URL; quote it because of the ? and &:
curl "https://playground.tetiva.app/get?page=2&limit=10"
The test API echoes them back under args. Add -i to see the status line and response headers too.
What the server sees: JSON or a form#
The most common curl mistake: the body arrives, but the server doesn't know it's JSON. The test API makes this visible. With the header, your data shows up under json; without it, it lands in form.
If the body didn't parse, check two things: the header, and whether the quotes survived. Invalid JSON such as {name:Tetiva} won't parse either.
Recent curl has a shortcut, --json. It sends the data as the body and sets both Content-Type: application/json and Accept: application/json:
curl --json '{"name": "Tetiva"}' https://playground.tetiva.app/post
The flag arrived in curl 7.82.0 in spring 2022. Check yours with curl --version; the copy bundled with older Windows 10 builds may predate it.
POST with form data or a file#
If the API expects a regular form rather than JSON, skip the header: -d sets Content-Type: application/x-www-form-urlencoded by itself. Separate the fields with &:
curl https://playground.tetiva.app/post \
-d 'name=Tetiva&qty=2'
To upload a file, use -F. curl then builds a multipart/form-data body, the way a browser does for a file upload, and picks POST on its own as well. The @ before the name means "read the contents of this file":
curl https://playground.tetiva.app/post \
-F 'file=@photo.jpg' \
-F 'name=Tetiva'
The test API returns the fields under form and the file under files.
Sending the body from a file#
When the JSON is long or full of quotes, put it in a file and pass it with @:
curl https://playground.tetiva.app/post \
-H "Content-Type: application/json" \
--data-binary @order.json
Why --data-binary and not -d @order.json? -d strips newlines from the file. That's harmless for JSON and breaks anything where line breaks matter. --data-binary sends the file byte for byte. Write @- instead of a file name and curl reads the body from standard input.
To put a shell variable into the JSON itself, use double quotes and escape the inner ones: -d "{\"name\": \"$NAME\"}". If the value may contain quotes, let jq build the body and pipe it in:
jq -n --arg name "$NAME" '{name: $name}' \
| curl --json @- https://playground.tetiva.app/post
Headers and tokens#
Tokens travel in the Authorization header. The test API checks one at /bearer:
curl https://playground.tetiva.app/bearer \
-H "Authorization: Bearer abc123"
Without the header the same URL answers 401 with missing Authorization header. If the server rejects a token you did send, see the Bearer token post.
Keeping the token in a shell variable helps: set TOKEN=abc123 and write -H "Authorization: Bearer $TOKEN". Mind the double quotes: inside single quotes the shell won't expand the variable and sends the literal text $TOKEN.
A command with a token in it stays in your shell history and in any chat you paste it into. The token in the screenshot post is about exactly that kind of leak.
For a username and password there's -u: curl -u user:passwd https://playground.tetiva.app/basic-auth/user/passwd, and curl builds the Basic header for you.
To see what actually went out, add -v: lines starting with > are the headers you sent, < the ones you got back. -i adds just the response headers to the output.
curl on Windows: cmd and PowerShell#
curl ships with Windows 10 from version 1803 and with Windows 11, but bash commands don't run there as written. In cmd, wrap the JSON in double quotes, escape the inner ones as \" and continue lines with ^. In Windows PowerShell 5.1, type curl.exe instead of curl.
Command Prompt (cmd). cmd doesn't understand single quotes. Wrap the JSON in double quotes and escape the inner ones with a backslash. Continue lines with ^:
curl https://playground.tetiva.app/post ^
-H "Content-Type: application/json" ^
-d "{\"name\": \"Tetiva\", \"qty\": 2}"
PowerShell. In Windows PowerShell 5.1, the version that comes with the system, curl isn't curl at all: it's an alias for Invoke-WebRequest, so curl's flags produce an error about the Headers parameter. Call the real thing as curl.exe, and continue lines with a backtick.
PowerShell can also drop the quotes inside your JSON on the way to curl.exe, and the server gets a broken body. A file avoids the quoting trouble in both shells: curl.exe ... --data-binary "@order.json" behaves the same in cmd, PowerShell and bash. In PowerShell, quote @order.json, because PowerShell treats @ as special syntax too.
PowerShell 7 has no curl alias, so plain curl already runs curl.exe, and from 7.3 on, JSON in single quotes reaches it intact.
Common errors#
| What you see | Why | Fix |
|---|---|---|
415 Unsupported Media Type | No Content-Type: application/json | Add the -H, or swap -d for --json |
400 Bad Request, "invalid JSON" | The shell ate the quotes in the body | Put the JSON in a file and send it with --data-binary @file |
| The server got a form instead of JSON | -d without the header | Add Content-Type |
401 Unauthorized | No Authorization header, no Bearer, or an expired token | Run with -v and look at what went out |
| In cmd, the second line runs as its own command | \ continues lines only in bash and zsh | Use ^ in cmd, or keep it on one line |
PowerShell complains about the Headers parameter | curl there means Invoke-WebRequest | Type curl.exe |
If curl never got a response and exited with code 6, 7, 28, 35 or 56, the body isn't the problem, the network is: how to find the layer where the request died.
Importing curl into Postman or another client, and back#
The easiest place to get a ready-made command is your browser. Open DevTools (F12), go to Network, right-click a request and choose Copy → Copy as cURL. Chrome on Windows offers both (cmd) and (bash); take the one that matches your shell. The command includes every header and cookie, your session along with them. If you need the whole page rather than one request, save a HAR file instead.
If a long command is awkward to edit in the terminal, paste it into an API client. Postman takes it through Import, where you paste the command as text. In Tetiva you paste it straight into the URL bar of an HTTP request: method, URL, headers, body and auth fill their tabs, and a toast lists what came across. If the paste overwrote fields you had filled in, the toast offers Undo.
To turn a request back into a command, use Copy as cURL in the menu next to Send. {{token}}-style variables are already substituted in the command, secret values included, so swap the token for a placeholder before you post it in a chat. The import and export docs explain how pre-request scripts and cookies end up in the command.
Try it yourself#
- Send the command from the top of this post and find your data under
jsonin the response. - Drop
-H "Content-Type: application/json"and watch the data move toform. - Call
/bearerwithout the header and with it: you should get401, then200. - Paste any of these commands into your API client and send it from there.
The test API page lists every practice endpoint, including slow responses and any status code you need.