API documentation speaks curl. Almost every reference page has a command you can copy, and the quickest way to learn what an endpoint really returns is to run it. But the raw response is the least useful view of it: a wall of JSON where the structure - which fields exist, how deep they sit, which are arrays - has to be worked out by eye.

This walkthrough runs three real, public, keyless APIs through JSON Keyper's online cURL runner and extracts the keys from each response. Each one turns out to have a shape worth knowing about before you write code against it, and in each case the key list shows it in seconds.

The workflow

  1. Open the cURL runner and paste the command. A preview shows the method, URL, headers, and body that will be sent.
  2. Click Execute. A proxy makes the request - browsers cannot call most APIs directly - and returns the status, headers, and body.
  3. Click Extract Keys. Every key path in the response appears in the output, and the Format menu switches it to typed paths, JSONPath, a tree, or a TypeScript type.

The proxy only reaches public addresses, gives up after 5 seconds, and handles responses up to 2 MB. The cURL runner page lists every limit and exactly what the API receives; the examples below all fit comfortably.

Example 1: a weather API that returns columns, not rows

Open-Meteo is a free forecast API that needs no key. This asks for current conditions and an hourly forecast for Berlin:

curl "https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current=temperature_2m,wind_speed_10m&hourly=temperature_2m&forecast_days=1"

It returns about 1 KB of JSON. Extracted as typed paths, with array indices collapsed, it comes to 23 lines:

latitude                      number
longitude                     number
generationtime_ms             number
utc_offset_seconds            number
timezone                      string
timezone_abbreviation         string
elevation                     number
current_units                 object
current_units.time            string
current_units.interval        string
current_units.temperature_2m  string
current_units.wind_speed_10m  string
current                       object
current.time                  string
current.interval              number
current.temperature_2m        number
current.wind_speed_10m        number
hourly_units                  object
hourly_units.time             string
hourly_units.temperature_2m   string
hourly                        object
hourly.time                   array
hourly.temperature_2m         array

Two design decisions jump out of that list that are easy to miss in the raw response:

  • The hourly forecast is columnar. There is no array of hour objects. hourly.time and hourly.temperature_2m are two parallel arrays of 24 values each, and the reading for an hour is the value at the same index in both. Code that expects hourly[0].temperature will not find it; you zip the arrays instead.
  • Units live beside the data, not in it. Every object has a twin with _units on the end and the same keys. That explains a pair that otherwise looks like a mistake: current.interval is a number (900) while current_units.interval is a string - it is the word "seconds".

Switching the format to TypeScript interface turns the same response into a type you can paste into a client. For a deeper walk through reading structure this way, see understanding nested JSON.

Example 2: sending JSON with a POST

Reading is half of most integrations. JSONPlaceholder is a public test API that accepts writes, which makes it a safe place to try one. curl's --json option sends a JSON body and sets both the Content-Type and Accept headers in one go:

curl --json '{"title": "Hello", "body": "First post", "userId": 1}' https://jsonplaceholder.typicode.com/posts

The preview shows it becoming a POST with both headers set, before anything is sent. The response is 201 Created:

{
  "title": "Hello",
  "body": "First post",
  "userId": 1,
  "id": 101
}

The new field is id - the server assigned it, which is exactly what a client needs to read back after creating something. (JSONPlaceholder only pretends to save: the same request returns 101 every time.) The same pattern works with -X POST, -H "Content-Type: application/json", and -d '...', which is how older documentation writes it.

Example 3: a search, and why one response is not enough

Open Library's search API takes a query string. curl's -G option turns -d data into URL parameters, and --data-urlencode encodes the spaces in the query for you:

curl -G https://openlibrary.org/search.json \
  --data-urlencode 'q=the lord of the rings' \
  -d limit=3 \
  -d fields=key,title,author_name,first_publish_year,ratings_average,subject

The preview shows the URL that will actually be requested, ending in ?q=the+lord+of+the+rings&limit=3&... - exactly what curl itself sends. The response has 26 key paths, and two of them are worth a second look:

  • The total appears twice. The response has both numFound and num_found, holding the same number - over a thousand results, at the time of writing. When an API carries two spellings of one field, pick one and use it everywhere; mixing them is how two parts of one codebase end up disagreeing.
  • offset is null. Generate a TypeScript type from this response and you get offset: null - a field that can apparently never hold a value.

That second point is the classic trap of building a type from one sample. So fetch the second page - the same command with -d offset=3 added - and paste both responses into JSON Diff. Its Structure view reports exactly one change:

offset    No longer null    null → number

offset was only null because the first page had no offset. The Changes view also gets the rest right: start and offset changed, and three books left the page while three others arrived - matched by Open Library's own key field rather than by position, so no book is mistaken for a changed version of another.

The lesson generalises: a type generated from one response describes that response. Two responses with different content - a first and a later page, an empty and a full record - tell you far more. The JSON to TypeScript page covers how merging several records marks fields optional and nullable.

Copying a command from your browser

The best source of a curl command is often the site you are already using. In Chrome, Edge, or Firefox, open DevTools, go to the Network tab, right-click a request, and choose Copy → Copy as cURL (Firefox lists it under Copy Value). You get the exact request the page made, headers and all.

Read it before you paste it anywhere. A request copied from a signed-in session includes your cookies and any authorization header, which together are enough to act as you. Delete the Cookie header (or the -b option) and any token you do not want to share; most public endpoints work without them. Anything left in the command passes through the proxy on its way to the API.

When a command fails

Most failures fall into a few cases, and the message tells you which:

  • A localhost, private, or VPN-only address is refused by design - run curl locally instead.
  • A slow endpoint times out after 5 seconds.
  • An API that limits requests per IP address may already have been reached by other traffic through the proxy's shared address. Send your own token, if the API offers one.
  • A command that reads files - -F, -T, -d @file - cannot work from a web page, and is refused with an explanation rather than sent in some other form.

Doing the same in a terminal

If you have curl and jq installed, the same key list is one pipe away:

curl -s "https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current=temperature_2m,wind_speed_10m&hourly=temperature_2m&forecast_days=1" \
  | jq -r 'paths | select(last | type == "string")
           | map(if type == "number" then "[\(.)]" else "." + . end)
           | join("") | ltrimstr(".")'

On the weather response it prints the same 23 paths. That is the better choice in scripts and CI; the browser route wins when you want types, a TypeScript interface, or a comparison without writing any of it. How to extract all keys from a JSON object compares the jq approach with JavaScript, Python, and Ruby, including two widely shared jq commands that quietly give the wrong answer.

Further reading