Integrations · Laravel
IPAPI.GR for Laravel
A middleware looks up the visitor with Laravel's Http client, caches the answer for an hour, and puts it on the request for your controllers and views. On paid plans it also refuses self-declared AI crawlers whose address is not a known AI crawler.
- Written for
- Laravel 11 and later, PHP 8.2+
- Runs in
- Web middleware group
- Client IP from
- $request->ip() with TrustProxies
01 · Setup
4 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.
-
Add the key to config
Read the key through config() so config caching keeps working.
config/services.phpPHP // config/services.php 'ipapi' => [ 'key' => env('IPAPI_KEY'), ], // .env // IPAPI_KEY=your_api_key_here -
Create the middleware
Private and reserved addresses are skipped, timeouts fail open, and "No Data" becomes null.
app/Http/Middleware/IpapiGeo.phpPHP <?php // app/Http/Middleware/IpapiGeo.php: adds the visitor's lookup to the request. namespace App\Http\Middleware; use Closure; use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; use Symfony\Component\HttpFoundation\Response; class IpapiGeo { private const LOOKUP = 'https://ipapi.gr/api/lookup/'; private const AI_CLAIM = '/GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-SearchBot|Claude-User|PerplexityBot|Perplexity-User/i'; public function handle(Request $request, Closure $next): Response { $ip = $request->ip(); // honours TrustProxies, see bootstrap/app.php $geo = null; // Private and reserved addresses have no public location: skip them. if ($ip && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { $geo = Cache::remember('ipapi:'.$ip, now()->addHour(), fn () => $this->lookup($ip)); } $request->attributes->set('geo', $geo); // Paid plans return risk flags: refuse self-declared AI crawlers whose address is not a known AI crawler. $risk = $geo['risk'] ?? null; if ($risk && preg_match(self::AI_CLAIM, (string) $request->userAgent()) && ! $risk['is_ai']) { abort(403, 'Unverified crawler'); } return $next($request); } private function lookup(string $ip): ?array { try { $response = Http::timeout(2)->get(self::LOOKUP.rawurlencode($ip), [ 'key' => config('services.ipapi.key'), ]); } catch (ConnectionException) { return null; // timeout or network error: fail open } if (! $response->ok()) { return null; } // Missing values come back as the string "No Data". $known = fn ($value) => in_array($value, [null, '', 'No Data'], true) ? null : $value; return [ 'country' => $known($response->json('country')), 'country_code' => $known($response->json('country_code')), 'city' => $known($response->json('city')), 'timezone' => $known($response->json('timezone')), 'asn' => $known($response->json('asn')), 'isp' => $known($response->json('isp')), 'risk' => $response->json('risk'), // ['is_ai' => …, 'is_crawler' => …, 'is_scanner' => …] on paid plans only ]; } } -
Register it
Append it to the web group, and trust your load balancer so $request->ip() is the visitor.
bootstrap/app.phpPHP <?php // bootstrap/app.php (Laravel 11 and later) use App\Http\Middleware\IpapiGeo; use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Middleware; return Application::configure(basePath: dirname(__DIR__)) // ->withRouting(...) ->withMiddleware(function (Middleware $middleware) { // Behind a load balancer: trust it, so $request->ip() is the visitor, not the balancer. $middleware->trustProxies(at: ['10.0.0.0/8']); $middleware->web(append: IpapiGeo::class); }) ->create(); -
Use it
The lookup is stored as a request attribute.
ControllerPHP // In a controller or view $geo = $request->attributes->get('geo'); // null when unknown $country = $geo['country_code'] ?? null; // e.g. "GR" $timezone = $geo['timezone'] ?? 'UTC';
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.
- Trust only your own proxies
- trustProxies(at: '*') lets any client choose its IP through X-Forwarded-For. List your load balancer's addresses instead.
- Failed lookups are retried
- Cache::remember does not keep a null result, so a failed lookup is tried again on the next request rather than cached for an hour.
- Localhost has no location
- In development the client is 127.0.0.1 or ::1. The snippets skip private and loopback addresses instead of spending a request on them.
- 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.
Does it work with Laravel 10?
The middleware class does. Register it in app/Http/Kernel.php under the web group instead of bootstrap/app.php.
Which cache store is used?
Your default store. With the file or database driver it works; Redis or Memcached are faster for many visitors.
Do I need a paid plan?
No. Location and network fields work with a free key. The crawler check needs the risk flags, which come with every paid plan; on a free key it 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).