**TL;DR — Cloudflare's bot protection treats most non-browser HTTP clients as bots, which they technically are.** That means legitimate API traffic from partner backends, monitoring tools, and webhook senders gets blocked alongside actual attacks. The fix isn't disabling bot protection. It's writing a narrow Skip rule that lets verified machine-to-machine traffic through specific endpoints based on a shared secret header, with rate limiting on top.
A client called me with a frustrating problem. Their partner integration — the one that pulls product inventory from their WooCommerce store every fifteen minutes — had stopped working. The partner was getting `403 Forbidden` from `/wp-json/wc/v3/products`. The same endpoint worked perfectly when the client tried it from their browser. Same auth credentials. Same URL. Different result.
The application logs were clean. WordPress wasn't returning the 403. Something upstream of WordPress was.
Where do you look first?
When a request fails before it reaches your application, the answer is almost always the WAF or CDN sitting in front of it. In this case Cloudflare. I logged into the client's Cloudflare dashboard and went to `Security → Events`, filtered by the partner's IP, and immediately saw the matching block events. Cloudflare's bot detection was firing on the partner's requests and serving a managed challenge, which the partner's backend had no way to solve, so they all came back as 403.
This is the part that catches a lot of people off guard. Cloudflare's bot management isn't broken when it blocks your partner's API client. It's working exactly as designed. The partner's backend genuinely is a bot — it's an automated process making programmatic HTTP requests, with no JavaScript runtime, no cookies, no human interaction patterns. That's the technical definition of a bot. Cloudflare doesn't have a way to know which bots are friendly and which aren't. You have to tell it.
Why IP allowlisting isn't always the answer
The first instinct most people have is "just allowlist the partner's IP." Sometimes that works. Often it doesn't, for one of three reasons.
- The partner runs in AWS or another cloud, so their IP rotates whenever they redeploy.
- The partner has multiple data centers and the requests come from a wide pool of addresses they can't promise stability on.
- You're integrating with a SaaS vendor whose IPs change without notice, and their docs literally say "do not allowlist us."
In this case it was reason 1. The partner ran on AWS Lambda and the source IPs cycled across the entire `us-east-1` block. Allowlisting wasn't an option.
The shared-secret header approach
The pattern I use for this kind of integration is a shared secret in a custom header. The partner sends every request with a header like `X-Partner-Auth: <long-random-string>`. Cloudflare has a Skip rule that says "if this header has the right value AND the request is hitting a specific path, skip bot protection and the WAF." Everything else still gets the full security stack.
In Cloudflare, this lives under `Security → WAF → Custom rules`. The rule expression looks like this:
(http.request.uri.path contains "/wp-json/wc/v3/products")
and (http.request.headers["x-partner-auth"][0] eq "REPLACE_WITH_LONG_RANDOM_STRING")Action: `Skip` → check `All managed rules`, `Bot Fight Mode`, `Super Bot Fight Mode`. That tells Cloudflare to let this specific kind of request through unchallenged, but only when *both* conditions are met. Either the path is wrong or the header is missing or the secret doesn't match, and the request gets the full security treatment.
Generate the secret with something like `openssl rand -hex 32` and store it both in Cloudflare and in the partner's environment variables. Rotate it every quarter or so.
The shared secret is not authentication. It's a routing signal that tells Cloudflare "this is the partner integration, treat it differently." The actual WordPress REST API still authenticates with its normal auth mechanism (application passwords, OAuth, JWT, whatever you've set up). Don't use the shared secret as the only thing standing between an attacker and your data — anyone who finds it gets to bypass your WAF, but they still need real API credentials to actually do anything.
Add rate limiting on top
The bot protection bypass means this endpoint no longer benefits from Cloudflare's automatic abuse mitigation. If your partner's credentials leak, an attacker could hit `/wp-json/wc/v3/products` as fast as they want without being challenged. To put a ceiling on that, add a rate limit rule that applies to the same path:
Path: /wp-json/wc/v3/products
Limit: 60 requests per minute per IP
Action: Block for 10 minutesSixty per minute is comfortably above the partner's normal rate (one request every fifteen minutes is, like, 0.07 RPM) but low enough that any abuse hits the ceiling almost immediately. You can be more or less generous depending on what the endpoint is and how spiky legitimate traffic is.
Why I prefer this over disabling protection on the path
The lazy version of this fix is a Page Rule that just turns off security for `/wp-json/*`. I see this everywhere and it's terrible. The WordPress REST API is an enormously attractive attack surface — content enumeration, user enumeration, auth brute-forcing, and a steady supply of plugin vulnerabilities all live behind those endpoints. Turning off the WAF for the entire `/wp-json/*` namespace because one specific endpoint needs to talk to one specific partner is a massive overreaction. The shared-secret skip rule is narrow on both axes (specific path AND specific header), so it carves out exactly the traffic that needs to bypass and nothing else.
What I actually told the client
Most clients don't want to hear about WAF rules and HTTP headers. They want to hear "the partner is working again, here's what we changed, here's what to do if it breaks again." So my actual writeup was three paragraphs: the partner is integrated through this skip rule, the rule depends on a shared secret stored in their env vars, here's how to rotate it. The technical detail above is for the engineer who has to maintain it later, not the person who wrote the cheque.

