We were load-testing our own inference API from a small Python script and
every request came back 403 Forbidden in 30 milliseconds. The same
request from curl worked. The API key was fine. Nothing in our gateway
logs, nothing in nginx. The requests were being refused before they reached
us.
If your API sits behind Cloudflare and some clients get a 403 that your application never sees, this is probably the same thing. Here is how to tell in one command, and the fix that keeps Cloudflare's protection where it belongs.
The tell: error code: 1010
Ask for the headers:
curl -s -i -A "Python-urllib/3.12" https://paraloncloud.com/v1/models \
-H "Authorization: Bearer prlc_…" | head -20
HTTP/2 403
server: cloudflare
cf-ray: a38506214b13f971-PRG
content-type: text/plain; charset=UTF-8
error code: 1010
Three things say it is the edge, not you:
server: cloudflareand acf-rayheader. Your origin's responses carry your server's header.- A plain-text body,
error code: 1010, where your API would have returned JSON. - Nothing in your logs. The request was answered at Cloudflare's edge.
The number matters. Cloudflare's 1010 is specifically Browser Integrity Check: "the owner of this website has banned your access based on your browser's signature". Other 10xx codes are other features; 1010 is this one.
What Browser Integrity Check does, and why it hits APIs
It looks at the HTTP headers of a request and blocks ones that are commonly
sent by abusive bots: a missing or odd User-Agent, missing headers a
browser would always send. It is a reasonable default for a website. It is
the wrong default for an API, because every API client is, by that
definition, not a browser.
Which clients trip it is arbitrary from the developer's point of view. We measured, same key, same endpoint:
| client User-Agent | result |
|---|---|
curl/8.5.0 | 200 |
python-requests/2.32.3 | 200 |
node | 200 |
okhttp/4.12.0 | 200 |
Python-urllib/3.12 | 403, code 1010 |
The OpenAI Python SDK uses httpx and was never affected, which is why we
did not notice for a while. A user with a twenty-line script on the
standard library was.
The fix: turn it off for the API path only
Do not turn Browser Integrity Check off for the whole zone; it is doing useful work on your pages. Scope it with a Configuration Rule:
- Cloudflare dashboard, your zone, Rules → Configuration Rules → Create rule.
- Name:
API: no browser integrity check. - When incoming requests match: URI Path · starts with ·
/v1/. - Then: find Browser Integrity Check in the settings list and set it to Off.
- Deploy.
It applies within seconds. Same request, after:
Python-urllib/3.12 on /v1/chat/completions 200
Python-urllib/3.12 on /v1/models 200
Python-urllib/3.12 on / 403 ← still protected
That last line is the point: the site keeps the check, the API does not.
Finding the setting in the new dashboard
If you are looking at the redesigned Security section and cannot find it: Security → Settings, and type "Browser integrity" into the page filter. It is its own row, separate from the bot settings. The dashboard also shows "1 configuration rule" next to it once the path rule exists, which is a quick way to confirm the rule is attached.
What not to do
- Do not enable Bot Fight Mode to "manage" this. On the Free plan it
cannot be scoped by path or skipped by a custom rule, and it challenges
exactly the traffic an API needs to accept: scripts, SDKs, and in our case
the agents on GPU nodes that hold a WebSocket to the hub with a
Go-http-clientuser agent. A challenge on that connection means nodes dropping off with no error they can act on. - Do not put the API on a different subdomain to escape the check if a Configuration Rule can do it. A path rule is one line and keeps one host.
- Do not assume "no 403 in the logs" means nobody is getting one. Edge blocks are invisible to your application by construction.
Keep it from coming back
The failure is silent, so make it loud: a scheduled request with
User-Agent: Python-urllib/3.12 against /v1/models, alerting on anything
but 200. A CDN setting can change with a dashboard click; a probe catches
it before a customer does. Ours is the one-line curl above, on a timer.
If you build against our API: nothing to change on your side. Any HTTP
client works now, standard library included. If you find one that does
not, tell us the cf-ray from the response header and we can see the
block on our side.


