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-Typewhat the body is,Acceptwhat you want back,Authorizationwho is calling. OpenAPI does not list these three as parameters:requestBody, the responses andsecuritydetermine 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: []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 —4is a number,"4"a string — and nests objects and arrays.application/x-www-form-urlencoded(WHATWG URL Standard): the classic HTML form format,name=valuepairs 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 inContent-Type; let the client generate it, because a hand-typedmultipart/form-datawithout 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--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
patchfor this path — a partial change. - Path: the server URL plus the path with both placeholders replaced:
/api/v1/customers/58/addresses/3. - Query:
notifysends the SMS and defaults totrue, so leaving it out would send one; add?notify=false. - Headers:
Content-Type: application/jsonfromrequestBody,Accept: application/jsonfrom the response, andAuthorizationwith the bearer token of a test account, fromsecurity. - Body: only the changed fields, typed as the schema defines them. The
$refpoints to theAddressChangeschema elsewhere in the document, which definescityandpostalCodeas strings, so the postal code goes in quotes:{"city": "Jeddah", "postalCode": "23436"}. - Variants: change one part at a time — send
postalCodeas a number, use an address that belongs to another customer, removeContent-Type— so each result points to one input.
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
parametersin the contract - 5–7Follow from
requestBody, the responses andsecurity
- 1MethodThe contract defines
patchfor this path: a partial change. - 2Path parameters
58and3replace{customerId}and{addressId}; both are required. - 3Query parameterOptional with the default
true; set tofalseso the test sends no SMS. - 4Server (base URL)The host of the contract's server URL, whose path
/api/v1starts the request path. Keep the base URL in a variable per environment. - 5Format headers
Content-Type: the body is JSON.Accept: send JSON back. - 6CredentialsThe bearer token of a test account, in a header and never in the URL.
- 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 astea & coffeearrives 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,
-dsends the body and makes the method POST unless-Xsets another; without aContent-Typeheader it labels the bodyapplication/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 typingmultipart/form-databy 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
notifycan 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
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.
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- Locate every input of the task in the reference: path, query, header or body.
- Write the method and the URL, encoding the query values.
- Add only the headers each request needs, and the body where there is one.
- Convert each request to cURL and compare it with the raw version.
Show the 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
- RFC 9110: HTTP SemanticsThe meaning of HTTP methods, header fields, status codes and content negotiation, independent of the wire version.
- OpenAPI SpecificationThe standard for describing REST APIs: paths, operations, parameters, schemas, responses and security schemes.
- URL StandardHow browsers actually parse, serialize and encode URLs, including application/x-www-form-urlencoded.
- RFC 7578: Returning Values from Forms: multipart/form-dataThe multipart/form-data encoding used for file uploads and mixed form submissions.
Practise with Mutqan tools
Compose and send HTTP requests (method, URL, headers, auth, body) and inspect the response — a lightweight Postman in your browser.
Build cURL commands from method, URL, headers, body and auth options.
Build correctly encoded query strings for API calls with pagination, filtering and sorting parameters.
Build request headers for REST APIs (auth, content negotiation, tracing) and export to cURL, fetch, axios or HTTP.
Compose JSON request bodies from a visual field editor and validate them before sending.
See exactly what your client sends: method, headers, query, body, IP and TLS info.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.