How to test API with curl

curl builds the whole request for you: curl -s -X POST https://api.example.com/orders -H 'Content-Type: application/json' -d '{"order":1042}' sends the method, the header and the body, then prints the response body on stdout. Add -i to keep the status line and headers, and -v to see what curl actually sent.

Why check this

Run this the first time an endpoint reaches staging, before anyone writes an automated test against it. curl sends the exact bytes a client would send, so a contract mismatch appears here instead of inside a failing suite three days later. The failure it prevents is specific: a POST answered with 200 while the service ignores the payload, because curl encoded the body as a form and the handler parses JSON.

Prerequisites

Steps

  1. Step 1.

    Send a GET with query parameters and the Accept header the client uses.

    curl -s -X GET 'https://httpbin.org/get?order=1042&status=open' -H 'Accept: application/json'
    
    {
    "args": {
      "order": "1042", 
      "status": "open"
    }, 
    "headers": {
      "Accept": "application/json", 
      "Host": "httpbin.org", 
      "User-Agent": "curl/8.21.0", 
      …
    }, 
    "url": "https://httpbin.org/get?order=1042&status=open"
    }

    args shows the parameters arrived parsed, not glued into one string.

  2. Step 2.

    Send a POST with a JSON body and declare the content type.

    curl -s -X POST 'https://httpbin.org/post' -H 'Content-Type: application/json' -d '{"order":1042,"qty":2}'
    
    {
    "data": "{\"order\":1042,\"qty\":2}", 
    "headers": {
      "Content-Length": "22", 
      "Content-Type": "application/json", 
      …
    }, 
    "json": {
      "order": 1042, 
      "qty": 2
    }, 
    …
    }

    A populated json object means the server parsed the body. A populated data with json: null means it received text it did not parse.

  3. Step 3.

    Add the credential the route requires and read the response.

    curl -s -w '\nstatus %{http_code}\n' 'https://httpbin.org/bearer' -H 'Authorization: Bearer h2check-demo-token'
    
    {
    "authenticated": true, 
    "token": "h2check-demo-token"
    }
    
    status 200

    Repeat the same command without the -H flag. The same route answered status 401 here, which confirms the guard is on the route and not on a gateway in front of it.

  4. Step 4.

    Print the request curl built, before trusting any of the above.

    curl -sv -o /dev/null -d '{"order":1042}' 'https://httpbin.org/post' 2>&1 | grep -E '^[<>]'
    
    > POST /post HTTP/2
    > Host: httpbin.org
    > User-Agent: curl/8.21.0
    > Accept: */*
    > Content-Length: 14
    > Content-Type: application/x-www-form-urlencoded
    > 
    < HTTP/2 200 
    < date: Fri, 11 Sep 2026 18:58:29 GMT
    < content-type: application/json
    < content-length: 437
    < server: gunicorn/19.9.0
    …

    Lines starting with > are what curl sent. This command carried JSON text under a form content type, which is the mistake step 2 avoids.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | A JSON body and nothing else | The request succeeded and curl hid the metadata | Add -i when you also need the status line and headers. | | > Content-Type: application/x-www-form-urlencoded | You sent -d without declaring JSON | Add -H 'Content-Type: application/json' and rerun. | | Empty output, no error text | -s silenced a transport failure | Use -sS, which keeps errors while hiding the progress meter. | | "json": null with a filled "data" | The server got the bytes and refused to parse them | Compare the content type you sent with the one the route documents. |

Common mistakes

Sign: The API returns 200 but the record is never created.Cause: curl sets Content-Type to application/x-www-form-urlencoded whenever -d is used without an explicit header. Frameworks that parse only JSON see an empty body, apply defaults and answer 200. Step 4 shows the header curl inserted.
Sign: curl sends POST although the command says -X GET.Cause: Any -d, --data-raw or -F flag switches the method to POST on its own. Writing -X GET after them produces a GET that still carries a body, which many servers drop silently. Remove the data flag instead of fighting it with -X.
Sign: The same command works in Git Bash and fails in PowerShell.Cause: PowerShell strips single quotes before curl sees them, so the JSON body arrives as a bare word and the server answers 400. Use double quotes with escaped inner quotes, or put the body in a file and pass -d '@body.json'.

What to check next

FAQ

How to test a REST API using a curl command?

Send one request per operation. -X names the method, -H adds each header, -d carries the body, and the response body lands on stdout. Read the status with -i or -w '%{http_code}'. Nothing else is required for a first contract check.

Can curl send PUT and DELETE?

Yes. curl -X PUT and curl -X DELETE work the same way, and -d attaches a body to either. For DELETE most routes take the identifier in the path, so the command is the URL plus the method.

How do I send a JSON file as the body?

Prefix the filename with an at sign: -d '@order.json'. curl reads the file as is. Keep -H 'Content-Type: application/json', because reading a body from a file does not change the content type curl sets.

How do I see the response headers and the body together?

Use -i. It prints the status line and the response headers, a blank line, then the body. -v adds the request side and the TLS handshake, which is what step 4 filters down to two symbols.

Verified

Verified by Maks Vernycurl 8.21.0

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

basic6 minpublished updated Maks Verny