Cloudflare error 1010 rejects Python's default User-Agent before the API's auth layer ever sees the request. How to tell it from a real 401, and the four headers that fix it."
Originally published on server over a vendor REST API, so an assistant could read job records without me pasting spreadsheet exports into a chat window. The vendor authenticates with the customer's own API key over HTTP Basic. Two lines of urllib. Done before lunch.
Every request came back the same way:
HTTP/1.1 403 Forbidden
error code: 1010
The short version
Cloudflare error 1010 is not an authentication failure. It is a browser-signature ban issued at Cloudflare's edge — is the cheapest fingerprint an edge can filter on. That string alone got every request I sent discarded before the vendor's servers ever saw it.
The fix
Four request headers. No proxy, no scraping framework, no third-party HTTP client.
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/126.0.0.0 Safari/537.36"
),
"Accept": "application/json, text/plain, */*",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
}
I proved it side by side with a key I made up on purpose:
| Headers | Result |
|---|---|
| Python default | 403 · error code: 1010 |
| Browser set above | 401 HTTP Basic: Access denied. |
A 401 was the win condition. A correct rejection of a deliberately fake credential meant the request had finally reached the application.
The honest caveat: this defeats User-Agent fingerprinting and nothing else. If the edge escalates to TLS or JA3 fingerprinting, urllib stops working again and the answer becomes a browser-impersonating client. I have not tested that, because it has not happened.
The trick I took away: 401 is evidence you can collect for free
The useful consequence showed up later. I had written tools against seven endpoint paths. Five of them I had never successfully called — I had them from documentation, which is a claim, not a measurement.
So I called all seven with the correct headers and no credential at all: account, jobs, companies, court cases, courts, employees, invoices.
Every one returned 401. Not one returned 404.
Those two codes answer different questions. defines 401 as a request lacking valid authentication credentials — the route exists and is refusing you.
That distinction confirmed all seven paths, using zero credentials and touching zero customer data. It is the cheapest verification step I know of, and I had never deliberately reached for it before. You can run it against an API before you have a key, before you have written a client, before you have permission to do anything at all.
It does not confirm everything. Proving a door exists is not the same as knowing what is behind it — the field mappings behind five of those tools are still inferred rather than captured, and they are labelled that way in the README until someone runs them against a live key.
One unrelated landmine, since it will cost somebody an afternoon
The mcp package on PyPI is at 2.0.0, and FastMCP is gone from it.
# stale — every tutorial I could find still says this
from mcp.server.fastmcp import FastMCP
# current
from mcp.server.mcpserver import MCPServer
The .tool() decorator and .run(transport="stdio") are unchanged, so the migration is one line. Checked against the SDK rather than against the tutorials, on 4 August 2026 — if you are reading this much later, check it again.
What I actually took from it
Identical failures are a signal, not noise. If varying the input does not vary the output, the input is not being read.
A written environment fact is still a hypothesis. My own handoff notes said neither sandbox could reach that host at all. That was wrong. The network route was always fine and the original blocker had only ever been a missing key — I nearly designed around a constraint that did not exist.
Ask the question that costs nothing first. The unauthenticated probe took ninety seconds and settled something I had planned to settle with a credential I did not yet have.
The server shipped read-only: ten tools, 28 unit tests, no mutating operations. Anything that writes back into a system of record stays behind a confirmation gate, which is its own problem and its own post.
I write these up as I go at , turned out to be the hard part — and a longer piece on how I work.
SOCIAL SHARE CARD GENERATOR