In this episode we'll send JSON bodies to a REST API complete with the Content-Type header, use the --json shorthand, process responses with jq, and build reproducible CRUD workflows and status code checks.

In the previous episode 5, you played with POST, PUT, DELETE, and form data — ways to send data to a server through the request body. Now it's time to level up: the data most often sent and received by modern APIs is JSON. Almost all production REST APIs speak JSON, from authentication to resource CRUD. This episode equips you with the techniques for sending JSON correctly, reading responses efficiently, and building repeatable testing workflows with consistent results.
Why is JSON so important? Because JSON is the lingua franca of data exchange between services — lightweight, human-readable, and natively supported by almost every programming language. If you master the combination of curl + JSON + jq, you already have a "swiss army knife" for testing any API without even opening Postman.
When sending a body, the server needs to know the format of that body so it can parse it correctly. The Content-Type header is the label that tells the server: "this body is JSON". Without that label, many web frameworks will reject or misinterpret the body — resulting in a 415 Unsupported Media Type error or data that's unreadable altogether.
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name": "Budi", "email": "budi@example.com"}'Notice two important parts above: -H "Content-Type: application/json" as the format label, and -d '{"name": "Budi"}' as the JSON payload. This combination is the most common one you'll see in API documentation and your CI/CD scripts.
There's a detail that often trips up beginners: -X POST is actually not mandatory. When curl detects -d, it automatically sends a POST request — but at the same time it adds the header Content-Type: application/x-www-form-urlencoded. That's why you must override that header to application/json, so the server reads the body as JSON, not as a regular form.
--json ShorthandWriting two or three flags over and over is boring and prone to typos. Since curl 7.82.0, there's a shorthand that simplifies everything: --json. This single flag does three things at once — sends a POST request, sets Content-Type: application/json, and attaches the payload.
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name": "Budi", "email": "budi@example.com"}'Both commands produce an identical request. --json also accepts many forms of input: literal strings, file paths with the @ prefix, and data from stdin. For complex or long payloads, save the JSON to a file then send it with --json @payload.json — far easier to maintain than writing hundreds of characters on one terminal line.
Tip
The @ prefix on -d and --json means "read file contents", not send a literal. So --json @payload.json reads the contents of payload.json as the body, while --json '{"name":"Budi"}' sends a literal string. The only difference is the @ prefix — and this applies to all data transfer options in curl.
Modern API responses are usually long, nested JSON that's hard to read when displayed raw. That's where jq comes in — a command-line utility to format, filter, and transform JSON. The curl + jq combination is one of the most productive pipelines you can learn in the shell.
curl -s https://api.example.com/users | jqWithout jq, a long response scrolls past with no structure. With jq, the JSON is pretty-printed — neatly indented and easy to scan. This pipeline works because curl -s prints the response body to stdout, then jq reads stdin and displays the formatted version.
To filter data, jq uses path-based filters:
curl -s https://api.example.com/users | jq '.[] | {name, email}'The .[] filter takes every element of the array, and {name, email} selects only the name and email fields. You can chain filters further: select(.role == "admin") to choose only admin users, or .length to count the elements. The more complex the response, the greater the value of jq.
Now let's assemble everything into a full REST testing workflow against a single resource — for example /users on your development API.
curl -s https://api.example.com/users | jq '.[] | {id, name}'curl -s --json '{"name": "Sari", "email": "sari@example.com"}' \
https://api.example.com/userscurl -s -X PUT https://api.example.com/users/42 \
-H "Content-Type: application/json" \
-d '{"name": "Sari Utami", "email": "sari@example.com"}'curl -s -X DELETE https://api.example.com/users/42Notice the pattern above: each operation represents one HTTP verb and one resource. GET to read, POST to create, PUT to update, DELETE to remove. Identical URL structure (/users/42) with different verbs is the essence of REST architecture — and curl is the right tool because it can freely call any verb.
Seeing the response body alone isn't enough to confirm a request succeeded. The HTTP status code is the source of truth: 200 means OK, 201 Created, 204 No Content, 400 Bad Request, 401 Unauthorized, 404 Not Found, and 500 Server Error. curl has the -w (write-out) feature to print extra information — including the status code — after a transfer finishes.
curl -s -o /dev/null -w "%{http_code}\n" https://api.example.com/users/42The -o /dev/null combination discards the body so it doesn't flood the terminal, then -w "%{http_code}" prints only the status number. This is a very common technique in monitoring scripts and CI: check whether an endpoint returns 200, and if not, mark it as failed.
Tip
To read the response and status at once, combine both: curl -s -w "\nHTTP %{http_code}\n" URL. The HTTP 200 line prints at the bottom of the output, so you still see the body and the status in one window — convenient for quick debugging.
A request is reproducible if it can be re-run at any time with predictable results. This is the key to reliable team testing. Some habits worth building:
--json @payload.json for large payloads so they're easy to review and change.-X DELETE or --json explicitly according to your intent.-w "%{http_code}" in scripts to make assertions.-s (silent) suppresses the progress meter; -o /dev/null discards the body when all you want is the status.URL="https://api.example.com/users"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
--json @payload.json "$URL")
echo "create status: $STATUS"In this episode 6, you've completed your REST testing skills: sending JSON bodies with -H "Content-Type: application/json" and -d '{"key":"value"}', simplifying with the --json shorthand, processing JSON responses through the jq pipeline, running a complete CRUD workflow, and checking status codes with -w "%{http_code}" for reproducible requests.
The most important takeaway: the curl + jq combination is a superpower — from a raw request straight to structured data ready for scripts. The more often you build the workflows above, the faster and more reliable your API testing becomes.
In the next episode 7, we'll shift from playing with data to playing with files — how to download files with the right name, resume interrupted downloads, upload files to a server, and access FTP and SFTP. See you!