Quick answer: Keep an API key secret by storing it in an environment variable or a secret manager, never in client-side code, git commits, logs, or URL query strings. Send it only from a server you control, scope it to the least access it needs, and rotate it on a schedule. If a key leaks, revoke it first and investigate second.
An API key is a password that happens to look like a random string. The whole security model rests on one assumption: only you have it. Every leak I have seen traces back to breaking that assumption in a way that felt harmless at the time.
Here is how keys actually get out, and how to make sure yours does not.
Why API Keys Leak
Keys rarely leak through some clever attack. They leak through convenience.
- Client-side code. You call an API from browser JavaScript or a mobile app and put the key in the request. Anyone can open dev tools or decompile the app and read it. Shipping a secret to a device you do not control is the same as publishing it.
- Git commits. You hardcode a key to test something, commit, and forget. Even if you delete it in a later commit, it lives forever in the history. Public repos get scraped by bots within minutes of a push.
- Logs. Your app logs the full request, including headers or the URL, and the key rides along into a log aggregator that half the company can read.
- Query strings. You put the key in the URL as
?api_key=.... Now it sits in browser history, server access logs, proxy logs, and theRefererheader sent to third parties. - Screenshots and support tickets. Someone pastes a curl command with a live key into a chat or a screenshot. It happens constantly.
The pattern is the same every time: a secret ends up somewhere it can be read by someone who should not read it.
Where to Store an API Key
The safe places all share one property: the key stays on a server you control and never reaches the end user.
| Storage method | Good for | Notes |
|---|---|---|
| Environment variables | Most apps, local dev | Simple, well supported, keeps keys out of code |
| Secret manager (AWS Secrets Manager, Vault, Doppler, 1Password) | Teams, production | Access control, audit logs, rotation support |
| CI/CD encrypted secrets | Build and deploy pipelines | GitHub Actions secrets, GitLab CI variables |
| Hardcoded in source | Nothing | Never do this |
| Client-side / frontend | Nothing | Never do this |
For a small project, environment variables are enough. For a team or anything in production, a secret manager earns its keep because it gives you access control and an audit trail of who read what.
The .env pattern
The most common setup is a .env file that stays on your machine and never gets committed.
# .env (this file is git-ignored, never commit it)
LINKSC_API_KEY=lsc_live_9f3a2c...
# .gitignore
.env
.env.local
.env.*.local
Then load it at runtime instead of hardcoding:
import os
import requests
api_key = os.environ["LINKSC_API_KEY"] # read from the environment
resp = requests.post(
"https://api.link.sc/v1/fetch",
headers={"x-api-key": api_key},
json={"url": "https://example.com", "format": "markdown"},
)
print(resp.json()["content"])
Commit a .env.example with the key names but no values, so teammates know what to set without ever seeing a real secret.
# .env.example (safe to commit)
LINKSC_API_KEY=
Send the Key Server-Side Only
This is the rule that trips people up most, so it is worth being blunt: your API key belongs on your backend, not in the browser.
If your frontend needs data from an API, the frontend calls your server, and your server calls the API with the key. The key never leaves your infrastructure.
// Your backend (Node/Express) - the ONLY place the key lives
app.post("/api/fetch-page", async (req, res) => {
const resp = await fetch("https://api.link.sc/v1/fetch", {
method: "POST",
headers: {
"x-api-key": process.env.LINKSC_API_KEY, // server-side env var
"content-type": "application/json",
},
body: JSON.stringify({ url: req.body.url, format: "markdown" }),
});
const data = await resp.json();
res.json({ content: data.content });
});
The browser hits /api/fetch-page, which never exposes the key. link.sc keys start with lsc_ and go in the x-api-key header (not Authorization: Bearer), and they are meant to be used exactly this way: server-side, in a header, never in a query string.
If you are building an automated pipeline rather than a user-facing app, the same rule applies. See our guide on how to automate web scraping for where the key fits in a scheduled job.
Scope Keys to the Least Access They Need
One key for everything is convenient and dangerous. If that key leaks, the blast radius is your entire account.
Better practice:
- Use separate keys per environment (development, staging, production). A leaked dev key should never touch production data.
- Use separate keys per service or app so you can revoke one without breaking the others.
- Where the provider supports it, restrict a key's permissions to only the endpoints it calls. A key that only needs to fetch should not be able to change billing.
Naming keys clearly (ci-pipeline, prod-web-app, local-tuan) pays off when you are staring at an audit log trying to figure out which one to kill.
Rotate Keys on a Schedule
Rotation limits how long a leaked key stays useful. A key you rotate every 90 days is worth far less to an attacker than one that has lived unchanged for three years.
A rotation that does not cause an outage looks like this:
- Generate a new key alongside the old one.
- Deploy the new key to your secret store.
- Confirm traffic is flowing on the new key.
- Revoke the old key.
Because both keys are valid during the overlap, nothing breaks. Automate this where you can; a rotation you have to remember to do is a rotation that will not happen.
What to Do If a Key Leaks
Speed matters more than tidiness. Act in this order.
- Revoke or rotate the key immediately. Do this before you understand how it leaked. A dead key cannot be abused.
- Check usage. Look at recent request logs and your billing for spikes or calls you did not make.
- Find the source. Search your git history and logs. Tools like
git log -p -S "lsc_"or a secret scanner (gitleaks, trufflehog) help. - Purge it from history if it is in git. Rotating the live key is the real fix; scrubbing history is cleanup so it does not leak again.
- Add a guardrail. Enable secret scanning on your repo so the next accidental commit gets caught before it merges.
Do not skip step one to investigate first. The investigation is worthless if the key is being drained while you read logs.
A Short Checklist
- Key in an environment variable or secret manager, never in code.
.envin.gitignore,.env.examplecommitted with empty values.- Key sent from your server, never from the browser or a mobile app.
- Key in the
x-api-keyheader, never in a URL query string. - Requests and errors logged without the key in them.
- Separate keys per environment, scoped to least access.
- Rotation on a schedule, with an overlap window.
- Secret scanning enabled on the repo.
None of this is exotic. It is the difference between a key that stays yours and a key that shows up in someone else's script. When you generate your first key on link.sc, start with the .env pattern above and you will already be ahead of most projects. The full request format is in the docs.
Ready to build with the web? Create a free link.sc account and get 500 credits a month to start fetching and searching, no credit card required.