Integrations · Cloudflare Workers
IPAPI.GR for Cloudflare Workers
A Worker in front of your site looks up each visitor's IP, refuses requests that claim to be an AI crawler from an address that isn't one, and forwards country and ASN to your origin as request headers.
- Written for
- Workers, ES module syntax
- Runs in
- A Worker route in front of your origin
- Client IP from
- CF-Connecting-IP
01 · Setup
3 steps, copy and paste.
Every lookup runs on your server or edge, so the key never reaches a browser. Each example times out quickly, caches answers per IP and lets the visitor through if the lookup fails.
-
Configure the Worker
Route the Worker in front of your site. It forwards every request to your origin after the check.
wrangler.jsoncJSONC // wrangler.jsonc { "name": "visitor-lookup", "main": "src/index.js", "compatibility_date": "2026-11-01", // Run in front of your site. The Worker forwards each request to your origin. "routes": [{ "pattern": "example.com/*", "zone_name": "example.com" }] } -
Store the key as a secret
Secrets are encrypted and exposed to the Worker as env.IPAPI_KEY, never to visitors.
ShellShell # Stored encrypted by Cloudflare, read as env.IPAPI_KEY npx wrangler secret put IPAPI_KEY npx wrangler deploy
-
Write the Worker
The Worker overwrites the X-Visitor-* headers, so a client can't pass its own values through to your origin.
src/index.jsJavaScript // src/index.js: Cloudflare Worker in front of your origin. // Store the key as a secret: npx wrangler secret put IPAPI_KEY const LOOKUP = 'https://ipapi.gr/api/lookup/'; const AI_CLAIM = /GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-SearchBot|Claude-User|PerplexityBot|Perplexity-User/i; // Per-isolate memory: short-lived, but it saves repeat lookups for busy visitors. const memo = new Map(); const TTL_MS = 10 * 60 * 1000; const known = (v) => (v === undefined || v === null || v === '' || v === 'No Data' ? null : v); async function lookup(ip, env) { const hit = memo.get(ip); if (hit && hit.expires > Date.now()) return hit.data; try { const res = await fetch(LOOKUP + encodeURIComponent(ip) + '?key=' + encodeURIComponent(env.IPAPI_KEY), { signal: AbortSignal.timeout(1500) }); if (!res.ok) return null; const data = await res.json(); if (memo.size > 5000) memo.clear(); memo.set(ip, { data, expires: Date.now() + TTL_MS }); return data; } catch { return null; // timeout or network error: fail open } } export default { async fetch(request, env) { const ip = request.headers.get('CF-Connecting-IP'); // set by Cloudflare, cannot be spoofed by the client const data = ip ? await lookup(ip, env) : null; // Paid plans return risk flags: refuse self-declared AI crawlers whose address is not a known AI crawler. const ua = request.headers.get('User-Agent') || ''; if (data && data.risk && AI_CLAIM.test(ua) && !data.risk.is_ai) { return new Response('Unverified crawler', { status: 403 }); } // Pass the answer to your origin as request headers. const upstream = new Request(request); upstream.headers.set('X-Visitor-Country', known(data && data.country_code) || ''); upstream.headers.set('X-Visitor-ASN', known(data && data.asn) || ''); upstream.headers.set('X-Verified-AI', data && data.risk ? String(data.risk.is_ai) : ''); return fetch(upstream); } };
02 · The response
What you get back.
Free keys return location and network data: country, country_code, region, city, zipcode, latitude, longitude, timezone, isp, organization and asn.
Keys on any paid plan add risk.is_ai, risk.is_crawler and risk.is_scanner, which tell you when an address belongs to a known AI crawler, search crawler or internet scanner. The examples use is_ai to refuse requests whose user agent claims an AI crawler while the address isn't one. false means "not a known bot", not "human".
All response fields · AI crawlers we recognise · Bot signal coverage
03 · Pitfalls
What usually goes wrong.
- Cloudflare already gives you a location
- request.cf carries Cloudflare's own country and city fields. If that is all you need, use them. Call the API when you want the same response fields as your other services, or the risk flags on paid plans.
- Isolate memory is short-lived
- The Map in the snippet lives only as long as the isolate, so it saves repeat lookups for busy visitors but is not a durable cache.
- Watch your daily limit
- A Worker sees every request, including assets. Restrict the route to HTML paths, or skip lookups for static files, if you are close to your plan's daily limit.
- Website restrictions block server calls
- A key restricted to "Allowed websites" needs an Origin or Referer header, which server-side code does not send, so the call is refused. For a server key, leave websites empty and restrict by your server's outgoing IP address instead.
- "No Data" is a string, not null
- When no value is known for a field, the field is still present and set to "No Data". Every snippet converts it to null before you use it.
04 · Questions
Good to know.
Why check the IP if the user agent says GPTBot?
Anyone can send that user agent. The risk.is_ai flag says whether the address really belongs to a known AI crawler or agent operator, so a copied header is not enough.
Can a visitor fake CF-Connecting-IP?
Not through Cloudflare: it sets the header to the address that connected to its edge.
Do I need a paid plan?
The country and ASN headers work with a free key. The AI-crawler check needs the risk flags, which come with every paid plan; on a free key that block is skipped.
Other integrations
Ship it today.
Free: 1.000 requests a day for location and network data. Bot flags and higher limits on every paid plan, from 9,00 € a month (VAT included).