Every call you made in the first two lessons carried a header you copied without much thought: x-api-key. Take it away and the server stops answering your questions. This lesson is about why, what each kind of rejection looks like, and how to handle a key so it never ends up somewhere it can be stolen.
What the key is for
A public website does not care who you are. An API usually does, for three reasons:
- Billing. Calls cost money to serve. The key tells the server whose account to charge.
- Limits. Your plan decides how many calls you can make per minute and per month. The key is how the server knows which plan applies.
- Control. A key can be restricted, revoked or replaced. If something goes wrong, the owner can shut off one key without touching anything else.
An APIVerve key is a long random identifier in UUID format: groups of letters and digits separated by dashes. One key covers your whole account, so the same key that calls Number to Words also calls Email Validator, Country Lookup and everything else in the catalog. There is no separate signup per API.
The important property is that a key is a bearer credential. Whoever holds it can use it. The server has no way to tell you from someone who copied your key out of a screenshot, so the key deserves the same care as a password. Most of this lesson is about that.
Sending the key
You have already done this. The key goes in a request header named x-api-key:
curl 'https://api.apiverve.com/v1/[email protected]' \
-H 'x-api-key: YOUR_API_KEY'{
"status": "ok",
"error": null,
"data": {
"email": "[email protected]",
"domain": "myspace.com",
"username": "support",
"isRegexValid": true,
"isMxValid": true,
"isValid": true,
"isFreeEmail": false,
"isCompanyEmail": true,
"isDisposable": false,
"isRoleAccount": true,
"canReceiveMail": true
}
}
(Trimmed. The full Email Validator response has more fields.) Run it yourself:
curl "https://api.apiverve.com/v1/emailvalidator?email=support%40myspace.com" \
-H "x-api-key: YOUR_API_KEY"isRoleAccount is true because support@ is a shared role address, not a person.
A few details about the header that save time later:
- Header names are case-insensitive.
x-api-key,X-API-KeyandX-Api-Keyare the same header. Lowercase is the convention. - There is an alternative form. Some HTTP tools only let you set an
Authorizationheader. APIVerve also acceptsAuthorization: Bearer YOUR_API_KEY, which resolves to the same account. Use one form or the other, not both. - Why a header and not the URL? URLs get written down everywhere: server logs, browser history, proxy logs, analytics tools, the address you paste into a chat. A key in a query string leaks into all of them. A header is still not secret from anyone who can see the request, but it is not recorded in nearly as many places by default.
Why is a header a better place for an API key than the query string?
Answer the check above to continue.
Keeping the key out of your code
Here is the rule: the key never appears in a file you commit. It lives in the environment, and your code reads it from there.
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]");
const res = await fetch(url, { headers: { "x-api-key": KEY } });
const body = await res.json();
if (body.status !== "ok") throw new Error(`${res.status}: ${body.error}`);
console.log(body.data.isValid, body.data.isRoleAccount);import os
import requests
KEY = os.environ["APIVERVE_API_KEY"] # KeyError if it is not set
res = requests.get(
"https://api.apiverve.com/v1/emailvalidator",
params={"email": "[email protected]"},
headers={"x-api-key": KEY},
timeout=15,
)
body = res.json()
if body["status"] != "ok":
raise RuntimeError(f"{res.status_code}: {body['error']}")
print(body["data"]["isValid"], body["data"]["isRoleAccount"])A .env file, and the order that matters
Typing export in every new terminal gets old. The usual answer is a file named .env in your project folder:
APIVERVE_API_KEY=YOUR_API_KEYNode 20.6 and later can load it directly with node --env-file=.env validate.mjs. In Python, the python-dotenv package does the same job.
Before you put a real key in that file, do this:
echo ".env" >> .gitignoreThe order matters. Git keeps every version of every file it has ever committed. If you commit .env once and then delete it, the key is still sitting in your repository's history, readable by anyone who can clone it. Removing it properly is far more work than not committing it. If a key has ever been pushed to a public repository, assume it has been copied, and replace it.
Why a key never goes in front-end code
Front-end code is the JavaScript that runs in your visitor's browser: a React app, a script tag on a static page. It is tempting to call an API straight from there. It is also the one place you can never keep a key secret.
Everything the browser runs, the visitor can read. Open the developer tools, go to the Network tab, click on any request, and every header is listed in plain text, x-api-key included. Minifying or bundling the code does not help: the key has to be in there, in readable form, for the browser to send it. A key in a browser bundle is a published key. Environment variables do not save you either: any variable a front-end build exposes to your code (the ones prefixed VITE_ or NEXT_PUBLIC_, for example) is copied into the bundle that gets shipped.
The fix is a small piece of your own server between the browser and the API. The browser calls your endpoint, your server adds the key and calls APIVerve, and only the result goes back:
import http from "node:http";
const KEY = process.env.APIVERVE_API_KEY;
if (!KEY) throw new Error("APIVERVE_API_KEY is not set");
http.createServer(async (req, res) => {
const email = new URL(req.url, "http://localhost").searchParams.get("email") ?? "";
const url = new URL("https://api.apiverve.com/v1/emailvalidator");
url.searchParams.set("email", email);
const upstream = await fetch(url, { headers: { "x-api-key": KEY } });
const body = await upstream.json();
res.writeHead(upstream.status, { "Content-Type": "application/json" });
res.end(JSON.stringify({ valid: body.data?.isValid ?? false }));
}).listen(3000);The browser now calls http://localhost:3000/?email=... and never sees the key. Your server decides what to pass back, which also means you can strip fields the browser does not need. In a real app you would add your own checks so that strangers cannot use your endpoint as a free proxy, but the principle is the whole point: the key stays on a machine you control.
You put your API key in a React app's environment variables and build the site. Is the key safe?
When a key leaks
It happens to everyone eventually. The response is the same every time:
- Rotate the key from the API keys page. Rotation creates a new key and invalidates the old one immediately. There is no grace period.
- Update everywhere that uses it: your
.env, your server's settings, your CI secrets. - Check your usage in the dashboard for calls you did not make.
For a key you are rotating on a schedule, you would roll the new one out first and rotate second, to avoid downtime. For a leaked key, invert that: rotate first and accept a few minutes of broken calls, because a leaked key is spending your credits for as long as it works.
Two habits reduce the damage when it does happen. Use one key per environment, so your laptop, staging and production each have their own and revoking one does not take down the others. APIVerve calls these additional keys sub-keys. And never log the key. If you need to know which key made a call, log the last four characters.
To check that a key works without spending anything, call the analytics endpoint, which every valid key can read and which is never billed:
curl 'https://api.apiverve.com/v1/analytics' -H "x-api-key: $APIVERVE_API_KEY"A 200 means the key is good; a 401 means it is not. The authentication docs cover sub-keys, scoping and rotation in more depth.
What you built, and what's next
You can now send a key correctly, recognise the three shapes of a 401 and tell a 401 from a 403, fail fast when a key is missing, keep the key in the environment and out of git, and keep it off the browser entirely with a small server in between.
Every request so far has been a GET with its inputs in the URL. The next lesson, GET vs POST and parameters, covers what happens when the input does not fit neatly in a URL: special characters that break query strings, and APIs that want a JSON body instead.