Sending requests with curl and Postman
The two tools you will use every day, and when to pick which.
So far you have sent requests from a browser and from the Send buttons on these pages. That works for simple GET requests and nothing else. To choose the method, set headers or send a body, you need a proper client. Almost every tester ends up using two: curl on the command line, and a graphical client such as Postman. This lesson covers enough of each to get through the rest of the tutorial.
curl
curl is a command-line program that sends a request and prints the response. It is installed by default on macOS, on most Linux systems and on Windows 10 and 11. Check with curl --version. It is also the format developers use to share requests: API documentation, browser developer tools and Postman can all show or export a request as a curl command, so you will need to read them even if you prefer another tool.
Without options, curl prints only the body. Add -i to include the status line and response headers, which you almost always want when testing:
curl -i https://api.testingapis.com/api/healthHTTP/1.1 200 OK
content-type: application/json; charset=utf-8
date: Sun, 11 Oct 2026 11:47:45 GMT
server: Kestrel
x-powered-by: TestingAPIs
transfer-encoding: chunked
{"status":"healthy","uptimeSeconds":76890,"timestamp":"2026-10-11T11:47:46Z"}For more detail, -v (verbose) also prints the request curl sent, marked with >, and the response headers marked with <. Lines starting with *describe the connection. Use it when a request doesn't behave the way you expect and you want to see exactly what went over the wire.
curl -v https://api.testingapis.com/api/health* Host api.testingapis.com:443 was resolved.
* IPv4: 135.13.233.203
* Established connection to api.testingapis.com (135.13.233.203 port 443)
* using HTTP/1.x
> GET /api/health HTTP/1.1
> Host: api.testingapis.com
> User-Agent: curl/8.18.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< content-type: application/json; charset=utf-8
< date: Sun, 11 Oct 2026 11:49:47 GMT
< transfer-encoding: chunked
...curl added three headers you didn't ask for: Host, User-Agent and Accept. Every client adds some defaults, and different clients add different ones. When the same request works in Postman and fails in curl, a default header is often the reason.
Method, headers and body
Three options cover most requests. -X sets the method. -H adds a header and can be repeated. -d sends a body. This sends a POST with a JSON body to an endpoint that echoes back what it received:
curl -X POST https://api.testingapis.com/api/methods/post \
-H "Content-Type: application/json" \
-d '{"name":"Asha","role":"tester"}'{"method":"POST","message":"POST request received","path":"/api/methods/post","query":{},"contentType":"application/json","bodyLength":31,"bodyTruncated":false,"body":"{\"name\":\"Asha\",\"role\":\"tester\"}","timestamp":"2026-09-30T18:31:16Z"}The backslash at the end of each line lets the command continue on the next line in bash and zsh. The body is wrapped in single quotes so the shell leaves the double quotes inside it alone. If you forget the Content-Type header, curl sends application/x-www-form-urlencoded, because -d was designed for HTML forms, and many APIs will reject the body.
curl 7.82 and later have a shortcut, --json, which sends the body, sets Content-Type: application/json and Accept: application/json, and switches the method to POST. This one creates a real todo:
curl --json '{"title":"Learn curl","completed":false,"userId":1}' \
https://api.testingapis.com/api/Todos{"message":"Todo created successfully","data":{"id":31,"userId":1,"title":"Learn curl","completed":false,"priority":"medium","dueDate":""}}The ID you get back will be different, because other people create todos too. For PUT or PATCH, add -X PUT or -X PATCH alongside --json.
Timing and scripting
-w prints values curl measured, after the response. Combined with -s (silent, no progress bar) and -o /dev/null (throw the body away), it gives you a one-line summary:
curl -s -o /dev/null \
-w "status %{http_code} total %{time_total}s size %{size_download} bytes\n" \
https://api.testingapis.com/api/Productsstatus 200 total 0.382512s size 5846 bytesOn Windows, use -o NUL instead of -o /dev/null. time_total includes DNS lookup, connecting and TLS, so the first request is often slower than the ones after it. The performance lesson breaks this number down further.
curl on Windows
Windows needs its own section, because the same command can behave three different ways depending on the shell.
In Windows PowerShell 5.1 (the version that ships with Windows), curl is not curl. It is an alias for Invoke-WebRequest, a PowerShell command with different options, so curl -i fails with a confusing error. Type curl.exe to run the real program. PowerShell 7 removed the alias, but typing curl.exeworks in both, so it's a good habit.
Quoting is the second problem. Windows PowerShell 5.1 strips double quotes inside arguments when it passes them to programs like curl.exe. The command below looks right, and the server receives broken JSON:
PS> curl.exe -s -X POST https://api.testingapis.com/api/methods/post -H "Content-Type: application/json" -d '{"name":"Asha"}'
{"method":"POST", ... "bodyLength":11, ... "body":"{name:Asha}", ...}The body arrived as {name:Asha}, which is not JSON. PowerShell 7.3 and later pass the quotes through correctly. On older versions, the least painful fix is to put the body in a file and point curl at it with @. The quotes around "@body.json" stop PowerShell from treating the @ as its own syntax.
# body.json contains {"name":"Asha","role":"tester"}
curl.exe -s --json "@body.json" https://api.testingapis.com/api/methods/postIn the old Command Prompt (cmd.exe), single quotes mean nothing, so wrap the body in double quotes and escape the ones inside with a backslash. The line continuation character is ^ instead of a backslash:
curl -X POST https://api.testingapis.com/api/methods/post ^
-H "Content-Type: application/json" ^
-d "{\"name\":\"Asha\",\"role\":\"tester\"}"Postman and other GUI clients
A graphical client does the same job as curl with a form instead of a command: pick the method from a dropdown, type the URL, fill in headers and body in tabs, click Send. The response appears below, formatted, with the status, time and size. Postman is the most widely used. Insomnia and Bruno work the same way, and Bruno stores everything as plain files, which some teams prefer because the requests can live in the same Git repository as the code. The ideas below apply to all of them, with Postman names.
Collections
A collection is a saved group of requests, usually one per API, with folders inside it. It is the reason to pick a GUI client over curl for a project you work on for months. You build up the requests once and share the collection with the team.
You don't have to build it by hand. This API publishes an OpenAPI document, a machine-readable description of every endpoint, at https://api.testingapis.com/openapi/v1.jsonsign in to see. In Postman, choose Import, paste that URL, and it creates a collection with a request for each endpoint. Most APIs you test at work will have a similar file, and it is the fastest way to get started on an unfamiliar one.
Environments and variables
Real projects run the same API in several places: a developer's laptop, a test server, staging, production. The requests are the same apart from the base URL and the credentials. Postman handles this with environments. Create one called "testingapis" with a variable baseUrl set to https://api.testingapis.com, then write your request URLs as {{baseUrl}}/api/Products/1. Switching environment switches every request at once. Put tokens in variables too, so they aren't copied into dozens of requests.
Tests in Postman
Each request has a Tests tab (called Scripts, with a post-response section, in newer versions) where you can write JavaScript that runs after the response arrives. For GET {{baseUrl}}/api/Products/1:
pm.test("status is 200", function () {
pm.response.to.have.status(200);
});
pm.test("product 1 has a price", function () {
const product = pm.response.json();
pm.expect(product.id).to.eql(1);
pm.expect(product.price).to.be.a("number");
});The results appear in a Test Results tab next to the response. The Collection Runner can then run every request in a collection and report which checks failed. This is a gentle first step into automation. The automation lesson covers writing tests as ordinary code, which scales better once you have more than a few dozen.
Which one to use
Both, for different jobs.
| Situation | Better choice |
|---|---|
| Checking one thing quickly, or reproducing a bug report | curl |
| Sharing an exact request in a ticket or chat | curl (anyone can paste and run it) |
| Exploring an API you don't know yet | GUI client with the OpenAPI import |
| Long JSON bodies you edit repeatedly | GUI client |
| Switching between test and staging servers | GUI client environments |
| Running inside a script or on a build server | curl, or tests written in code |
| Looking at raw headers and connection details | curl -v |
One more option for this API specifically: each endpoint page in the API reference has a form for sending the request and shows a ready-made curl command you can copy. That is often the quickest way to try an endpoint before you save it anywhere.
- When a bug report includes a request, attach it as a curl command. The developer can run it without importing anything, and there is no doubt about which headers were sent.
- Postman can convert in both directions. Paste a curl command into the Import dialog to get a request, or use the Code button on a request to get curl.
Exercise
Do these with curl, then repeat them in a GUI client.
- Send
GET /api/echo/headersand compare the headers curl sends with the ones your GUI client sends. Which headers did each tool add on its own? - Create a todo with
POST /api/Todos, then fetch it withGET /api/Todos/followed by the ID from the response. - Send the same POST without the
Content-Typeheader. What status and body come back? - Use
-wto time/api/Productsand/api/Products?delay=2. Thedelayparameter makes the server wait, so the second should take about two seconds longer.
In your GUI client, create an environment with a baseUrl variable and write one test that fails on purpose, so you know what a failure looks like before you rely on a pass.