Building API Requests

Building an API request means turning the API documentation into one exact HTTP message: a method, a URL, and every input in the place the API expects it — the path, the query string, a header or the body — encoded the way the API declares. A value in the wrong place may be ignored, or may change what the server does.

For example, "the paid orders of customer 58, page 2" becomes GET /api/v1/customers/58/orders?status=paid&page=2 with no body. "Change the city on address 3 of that customer" becomes PATCH /api/v1/customers/58/addresses/3 with Content-Type: application/json and the body {"city": "Jeddah"}.

Why it matters

A request built wrong tests nothing, or tests something else. Put a filter in the body of a GET and many servers ignore it and return the full list, so "the list loads" passes while the filter was never exercised. Leave an & unencoded in a search term and the server receives two parameters instead of one.

Controlling every part of the request also makes real defects easier to pin down: you change one input at a time, reproduce a bug without the user interface, and give developers a request they can run.

Four places an input can go

Start from the API reference. Many teams publish it as an OpenAPI document: a standard, machine-readable description of a REST API, written in YAML or JSON. There each parameter has an in field that names its location, such as path, query or header, and the body is described under requestBody. The excerpt below is the endpoint of the worked example.

  • Path parameters identify the resource, like {customerId} in /customers/{customerId}. They are always required; replace the whole placeholder, braces included.
  • Query parameters refine the request: filter, sort, page, search or switch an option. They are usually optional with a default, so leaving one out is a test case too. Percent-encode reserved characters in values.
  • Headers describe the request: Content-Type what the body is, Accept what you want back, Authorization who is calling. OpenAPI does not list these three as parameters: requestBody, the responses and security determine them.
  • The body carries the data you create or change. RFC 9110 gives content in a GET or DELETE request no generally defined meaning, so those requests normally have none.
# server: https://api.example.com/api/v1
/customers/{customerId}/addresses/{addressId}:
  patch:
    parameters:
      - name: customerId
        in: path
        required: true
        schema: { type: integer }
      - name: addressId
        in: path
        required: true
        schema: { type: integer }
      - name: notify
        in: query
        required: false
        description: Send the customer an SMS about the change
        schema: { type: boolean, default: true }
    requestBody:
      required: true
      content:
        application/json:
          schema: { $ref: '#/components/schemas/AddressChange' }
    responses:
      '200':
        description: The updated address
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Address' }
    security:
      - bearerAuth: []

Try it: OpenAPI Viewer

Request bodies: JSON, form and multipart

The Content-Type header must describe the bytes you actually send. Three formats cover most APIs; the sample sends the same rating in each.

  • application/json: the usual format for REST APIs. It keeps types — 4 is a number, "4" a string — and nests objects and arrays.
  • application/x-www-form-urlencoded (WHATWG URL Standard): the classic HTML form format, name=value pairs joined by &, percent-encoded, with a space written as +. Every value is text. Some login and token endpoints require it.
  • multipart/form-data (RFC 7578): for file uploads. The parts are separated by a boundary declared in Content-Type; let the client generate it, because a hand-typed multipart/form-data without the boundary cannot be parsed.
  • A format the endpoint does not accept should be refused, typically with 415 Unsupported Media Type — a useful negative test.
POST /api/v1/stores/riyadh-01/feedback HTTP/1.1
Host: api.example.com
Content-Type: application/json

{"rating": 4, "subscribe": true}

POST /api/v1/stores/riyadh-01/feedback HTTP/1.1
Host: api.example.com
Content-Type: application/x-www-form-urlencoded

rating=4&subscribe=true

POST /api/v1/stores/riyadh-01/feedback HTTP/1.1
Host: api.example.com
Content-Type: multipart/form-data; boundary=x7Kq9

--x7Kq9
Content-Disposition: form-data; name="rating"

4
--x7Kq9
Content-Disposition: form-data; name="photo"; filename="box.jpg"
Content-Type: image/jpeg

(the bytes of the image)
--x7Kq9--

Try it: API Payload Builder Try it: Request Body Inspector

Worked example: from the contract to the request

A test case says: change the city of address 3 of test customer 58 to Jeddah and the postal code to 23436, without sending the customer an SMS. Work through the excerpt above, one input at a time.

  • Method: the contract defines patch for this path — a partial change.
  • Path: the server URL plus the path with both placeholders replaced: /api/v1/customers/58/addresses/3.
  • Query: notify sends the SMS and defaults to true, so leaving it out would send one; add ?notify=false.
  • Headers: Content-Type: application/json from requestBody, Accept: application/json from the response, and Authorization with the bearer token of a test account, from security.
  • Body: only the changed fields, typed as the schema defines them. The $ref points to the AddressChange schema elsewhere in the document, which defines city and postalCode as strings, so the postal code goes in quotes: {"city": "Jeddah", "postalCode": "23436"}.
  • Variants: change one part at a time — send postalCode as a number, use an address that belongs to another customer, remove Content-Type — so each result points to one input.
In one PATCH request, path parameters pick the customer and address, a query parameter switches off the SMS, headers describe the body and the caller, and the JSON body carries only the changed fields.

Request

Part 1:PATCH /api/v1/customers/Part 2:58/addresses/Part 2:3?Part 3:notify=false HTTP/1.1
Part 4:Host: api.example.com
Part 5:Content-Type: application/json
Accept: application/json
Part 6:Authorization: Bearer eyJhbGciOi...

Part 7:{"city": "Jeddah", "postalCode": "23436"}
  • 2–3Declared under parameters in the contract
  • 5–7Follow from requestBody, the responses and security
  1. 1MethodThe contract defines patch for this path: a partial change.
  2. 2Path parameters58 and 3 replace {customerId} and {addressId}; both are required.
  3. 3Query parameterOptional with the default true; set to false so the test sends no SMS.
  4. 4Server (base URL)The host of the contract's server URL, whose path /api/v1 starts the request path. Keep the base URL in a variable per environment.
  5. 5Format headersContent-Type: the body is JSON. Accept: send JSON back.
  6. 6CredentialsThe bearer token of a test account, in a header and never in the URL.
  7. 7BodyOnly the changed fields, as JSON; the postal code is a string.
Text version
PATCH /api/v1/customers/58/addresses/3?notify=false HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
Authorization: Bearer eyJhbGciOi...

{"city": "Jeddah", "postalCode": "23436"}

Try it: API Query Parameter Builder Try it: API Header Builder

Sending it with an API client

A graphical API client — Postman, or the Mutqan API Request Builder in the browser — turns these decisions into fields: method, URL, a parameter table, a header table, authorisation and a body editor. cURL, a command-line client, turns them into options such as -X, -H and -d. The habits matter more than the tool:

  • Keep the base URL, test account and token in variables (Postman writes {{baseUrl}}), so one saved request runs on every environment.
  • Enter query parameters in the table and pick the body type, so the client builds the query string and sets Content-Type; if you also type that header, make the two agree. Clients differ in which characters they encode, so check that a value such as tea & coffee arrives as one parameter.
  • Check what was actually sent: clients add headers you did not type, such as User-Agent. Send the request once to a capture URL of the Mutqan HTTP Request Inspector and compare.
  • Export the request as cURL for bug reports, with the token replaced by a shell variable such as $TOKEN. The sample uses bash quoting; the Mutqan cURL Generator also writes PowerShell and Windows cmd versions.
  • In cURL, -d sends the body and makes the method POST unless -X sets another; without a Content-Type header it labels the body application/x-www-form-urlencoded, even when it is JSON.
curl -X PATCH "https://api.example.com/api/v1/customers/58/addresses/3?notify=false" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"city": "Jeddah", "postalCode": "23436"}'

Try it: API Request Builder Try it: cURL Generator Try it: HTTP Request Inspector

Common mistakes

  • Putting filters or search terms in the body of a GET request instead of the query string.
  • Typing query values into the URL without percent-encoding them, such as a + that many servers read as a space; the lesson on URLs, path and query parameters shows what to encode.
  • Sending JSON without Content-Type: application/json, or typing multipart/form-data by hand without its boundary.
  • Quoting numbers and booleans ("4", "true") that the schema defines as numbers or booleans.
  • Omitting optional parameters without knowing their defaults: an omitted notify can send real messages from a test.

The QA perspective

You build requests at three moments. In review: if you cannot build a valid request from the documentation alone, the documentation has a defect. In testing: one saved request per scenario, with variables for the environment and the data. In a bug report: the exact request as cURL, with the token removed.

Ask the developers early about what the contract leaves open — required headers such as a correlation id that links a request to the server logs, the default of each optional parameter, repeated keys or comma-separated lists — and record the answers in the saved requests (a Postman collection, for example) or the test case.

Practice exercise

Build two requests from an API reference

Your task

Using the reference below, build two requests: (1) search store riyadh-01 for "tea & coffee" in the categories drinks and gifts, cheapest first, which continues the search you encoded in the lesson on URLs, path and query parameters as a complete request; (2) post a 4-star review with the comment "Fresh, well packed" for product 9917 of that store.

Write each as raw HTTP and as cURL. Optionally, create a capture URL in the HTTP Request Inspector, use it in place of the base URL, send both requests there (with a dummy token and no real personal data) and compare what arrived.

Material
Base URL: https://api.example.com/api/v1
Every call: Authorization: Bearer <token>

GET /stores/{storeId}/products
  storeId    path   string   required
  q          query  string   optional  free-text search
  category   query  string   optional  repeat the key for several values
  sort       query  string   optional  price_asc | price_desc (default: relevance)
  Response: application/json

POST /stores/{storeId}/products/{productId}/reviews
  storeId    path   string   required
  productId  path   integer  required
  Body: application/json
    rating   integer  required  1 to 5
    comment  string   optional  up to 500 characters
Steps
  1. Locate every input of the task in the reference: path, query, header or body.
  2. Write the method and the URL, encoding the query values.
  3. Add only the headers each request needs, and the body where there is one.
  4. Convert each request to cURL and compare it with the raw version.
Show the model answer
Model answer

Request 1 as raw HTTP: GET /api/v1/stores/riyadh-01/products?q=tea%20%26%20coffee&category=drinks&category=gifts&sort=price_asc HTTP/1.1 with Host: api.example.com, Accept: application/json and Authorization; no body and no Content-Type. The query reuses the encoded search from the URL lesson; what this request adds is the choice of headers: Accept because the reference names a JSON response, Authorization because every call needs it, and nothing that describes a body. A client may write the spaces as + (q=tea+%26+coffee), the form-encoded spelling; a server that reads the query as form data decodes it to the same value.

Request 2 as raw HTTP: POST /api/v1/stores/riyadh-01/products/9917/reviews HTTP/1.1 with Host, Content-Type: application/json, Accept and Authorization, and the body {"rating": 4, "comment": "Fresh, well packed"} — 4 as a number, not the string "4".

cURL for request 1: curl "https://api.example.com/api/v1/stores/riyadh-01/products?q=tea%20%26%20coffee&category=drinks&category=gifts&sort=price_asc" -H "Accept: application/json" -H "Authorization: Bearer $TOKEN". Quote the URL, or the shell treats & as an operator. GET is the default method, so no -X is needed.

cURL for request 2: curl "https://api.example.com/api/v1/stores/riyadh-01/products/9917/reviews" -H "Content-Type: application/json" -H "Accept: application/json" -H "Authorization: Bearer $TOKEN" -d '{"rating": 4, "comment": "Fresh, well packed"}'. -d already makes the method POST, so -X POST is optional; the Content-Type header stops cURL from labelling the JSON as form data.

In the inspector, the query should decode to q = "tea & coffee" with two category values, and the review body should parse as JSON.

Frequently asked questions

How does an OpenAPI reference say how to send a list in the query?

Through style and explode on the parameter. For a query parameter the default is style: form with explode: true, which repeats the key: ?category=drinks&category=gifts. With explode: false the values are joined by commas: ?category=drinks,gifts. If the parameter sets neither, those defaults apply. A plain-text reference, like the one in the exercise, says it in words ("repeat the key"); if it says nothing, ask, because servers differ.

What do I do when the reference and the server disagree?

Follow neither by default: the mismatch is the finding. Record the request built from the reference, the response and the reference text, and ask the team which one is intended. Then either the server is fixed or the reference is corrected; only after that, update your saved request, so it keeps testing the contract rather than whatever the server does today.

Should a request body be JSON or form-encoded?

Whatever the contract declares for that endpoint. Most REST APIs expect application/json; HTML form posts and some token endpoints expect application/x-www-form-urlencoded; file uploads use multipart/form-data. The Content-Type header must match the body you send.

Why does a copied cURL command fail on Windows?

Shells quote differently. The bash version in this lesson wraps the JSON body in single quotes, which Windows cmd does not treat as quotes, so the command breaks. In Windows PowerShell 5.1, curl is an alias for Invoke-WebRequest, which does not accept cURL options; type curl.exe instead. The Mutqan cURL Generator writes the same request for PowerShell and cmd.

References

Practise with Mutqan tools

Compose and send HTTP requests (method, URL, headers, auth, body) and inspect the response — a lightweight Postman in your browser.

API Testing

Build cURL commands from method, URL, headers, body and auth options.

Web

Build correctly encoded query strings for API calls with pagination, filtering and sorting parameters.

Web

Build request headers for REST APIs (auth, content negotiation, tracing) and export to cURL, fetch, axios or HTTP.

Web

Compose JSON request bodies from a visual field editor and validate them before sending.

Web

See exactly what your client sends: method, headers, query, body, IP and TLS info.

API Testing

Go deeper

Your progress on this topic

Part of the QA Roadmap