Here is a bug that has shipped in a lot of real software. A signup form checks email addresses with an API, and it works for every address anyone tests. Then a user signs up with [email protected], a perfectly valid address, and the check reports something odd about an address with a space in it.
Nothing is wrong with the API. The + never arrived. To see why, and to stop it happening in your own code, you need to know how parameters travel inside a request. That is this lesson.
Two places to put your inputs
An HTTP request can carry inputs in two main places:
- The URL, in the query string after the
?. This is what every call so far has used. - The body, a block of data sent after the headers. A body can hold far more than a URL and can carry structured data like nested objects and lists.
Which one an API uses is tied to the method, the verb at the start of the request:
- GET means "read". Inputs go in the query string. There is no body.
- POST means "here is some data, process it". Inputs go in the body, usually as JSON.
There are other methods (PUT, PATCH, DELETE) that you will meet in APIs that store records for you. For the APIs in this course, GET and POST cover everything.
GET and the query string
The Unit Converter API takes three inputs, value, from and to, all as query parameters:
curl 'https://api.apiverve.com/v1/unitconverter?value=5&from=km&to=mi' \
-H "x-api-key: $APIVERVE_API_KEY"{
"status": "ok",
"error": null,
"data": {
"result": { "result": 3.11, "from": "5 km", "to": "mi" },
"unitDefinitions": {
"from": { "abbr": "km", "measure": "length", "system": "metric", "singular": "Kilometer", "plural": "Kilometers" },
"to": { "abbr": "mi", "measure": "length", "system": "imperial", "singular": "Mile", "plural": "Miles" }
}
}
}
The structure of the query string is simple: ? starts it, each parameter is name=value, and & separates them. The order does not matter, so ?to=mi&from=km&value=5 asks exactly the same question.
curl "https://api.apiverve.com/v1/unitconverter?value=5&from=km&to=mi" \
-H "x-api-key: YOUR_API_KEY"The converted number is at data.result.result. Swap from and to and run it again.
Everything in a query string is text
A URL has no types. value=5 sends the characters 5, and the server converts them to a number because it knows value is numeric. The same goes for booleans: a parameter documented as true/false arrives as the four letters true or the five letters false. This matters less for GET than for POST, as you will see shortly, but it explains why a query string can never send a list or a nested object without some agreed convention on top.
URL encoding, and the characters that break things
Back to [email protected]. In a query string, several characters already have a job:
| Character | Its job in a URL | Write it as |
|---|---|---|
& | Separates parameters | %26 |
= | Separates a name from its value | %3D |
+ | Means a space in a query string | %2B |
| space | Not allowed in a URL at all | %20 (or +) |
# | Starts the fragment, which is never sent to the server | %23 |
? | Starts the query string | %3F |
So [email protected] does not mean what it looks like. The + is read as a space, and the server receives jane.doe [email protected]. The fix is URL encoding (also called percent-encoding): replace each special character with % followed by its code. Encoded, the address is jane.doe%2Bnews%40gmail.com and arrives intact. (@ becomes %40; it is usually tolerated unencoded, but an encoder will escape it to be safe.)
The other characters fail in their own ways. An & inside a value, as in Fish & Chips, splits it into two parameters and the second half is lost. A # silently cuts off everything after it before the request even leaves your machine.
You should almost never type these codes yourself. Every language has an encoder, and the rule is: build query strings with the encoder, never by gluing strings together.
const KEY = process.env.APIVERVE_API_KEY;
if (!KEY) throw new Error("APIVERVE_API_KEY is not set");
const url = new URL("https://api.apiverve.com/v1/emailvalidator");
url.searchParams.set("email", "[email protected]");
console.log(url.href); // ...emailvalidator?email=jane.doe%2Bnews%40gmail.com
const res = await fetch(url, { headers: { "x-api-key": KEY } });
const { data } = await res.json();
console.log(data.isSubaddressed, data.normalizedEmail);import os
import requests
res = requests.get(
"https://api.apiverve.com/v1/emailvalidator",
params={"email": "[email protected]"}, # requests encodes this for you
headers={"x-api-key": os.environ["APIVERVE_API_KEY"]},
timeout=15,
)
print(res.url) # shows the encoded URL that was actually sent
data = res.json()["data"]
print(data["isSubaddressed"], data["normalizedEmail"])Both print true (or True) and [email protected]. The validator recognises the +news part as a sub-address, a tag Gmail ignores when delivering, and gives you the normalised address you would use to spot the same person signing up twice. None of that works if the + is lost on the way.
curl "https://api.apiverve.com/v1/emailvalidator?email=jane.doe%2Bnews%40gmail.com" \
-H "x-api-key: YOUR_API_KEY"isSubaddressed is true and normalizedEmail drops the +news tag. The Run button encodes the value for you.
From curl, -G with --data-urlencode does the encoding:
curl -G 'https://api.apiverve.com/v1/emailvalidator' \
--data-urlencode '[email protected]' \
-H "x-api-key: $APIVERVE_API_KEY"--data-urlencode normally builds a body; -G tells curl to put it in the query string of a GET instead.
You build a URL by hand: [email protected]. What does the server receive as the email?
Answer the check above to continue.
How to know which method an API wants
You do not guess. Each API's reference page states its method, and a call with the wrong one is rejected with a 405 Method Not Allowed rather than failing silently.
A rough rule of thumb for why an API is one or the other:
| The input is... | Usually | Example |
|---|---|---|
| A few short values: a number, a code, an address | GET | Unit Converter, Email Validator, Country Lookup |
| Free text that could be long | POST | Slugify, Sentiment Analysis |
| Structured: lists, nested options | POST | HTML to PDF, JSON to CSV |
| A file | POST (multipart upload) | Image to Text |
One thing that surprises people coming from other APIs: in a lot of web development, POST means "create a record". Here, POST endpoints are transformations. Sending the same body twice gives the same answer twice and creates nothing. The only cost of a duplicate is the extra credit.
Optional parameters and their defaults
Slugify has one required parameter, text, and two optional ones, separator and lowercase, which default to - and true. Leave an optional parameter out and you get its default. Two cautions follow from the request docs:
- Omitting a parameter is not the same as sending it empty.
"separator": ""is a value, and some endpoints reject it. - A misspelled parameter is ignored, not rejected. Send
seperatorand the call succeeds with the default-, as if you had sent nothing. When an option seems to have no effect, compare its spelling with the reference page before anything else.
What you built, and what's next
You sent inputs both ways: in an encoded query string on a GET, and as a typed JSON body on a POST with the right Content-Type. You saw why +, & and # break hand-built URLs and why the encoder fixes all of them, and you know where to look up an API's method instead of guessing.
So far, most of your calls have worked. The last lesson, Status codes and errors, is about the ones that do not: what each status code means on a real API, how to tell a call worth retrying from one that will fail forever, and the error-handling function you will reuse in everything you build after this.