Ipstack Laravel Integration
Laravel 12 Tech Tutorial: Real-World IP Intelligence with sudiptpa/ipstack + sudiptpa/guid
In this tutorial, we’ll build a production-style IP verification module in Laravel 12 using:
- sudiptpa/ipstack for geolocation intelligence
- sudiptpa/guid for request trace IDs
This is not just package installation. We’ll implement:
- Single IP lookup
- Requester IP lookup (`/check`)
- Optimized lookup with options
- Bulk lookup with free-plan fallback
- Web + API endpoints
- Persistence for observability and auditing
1. Install Packages
composer require sudiptpa/ipstack sudiptpa/guid
2. Configure Environment
Add to .env:
IPSTACK_ACCESS_KEY=your_access_key
IPSTACK_BASE_URL=https://api.ipstack.com
IPSTACK_TIMEOUT=10
IPSTACK_ENABLE_ADVANCED_OPTIONS=false
- Keep IPSTACK_ENABLE_ADVANCED_OPTIONS=false on free plans.
- Turn it on for paid plans if you need advanced fields/features.
Create ipstack.php:
<?php
return [
'base_url' => env('IPSTACK_BASE_URL', 'https://api.ipstack.com'),
'access_key' => env('IPSTACK_ACCESS_KEY'),
'timeout' => (int) env('IPSTACK_TIMEOUT', 10),
'enable_advanced_options' => (bool) env('IPSTACK_ENABLE_ADVANCED_OPTIONS', false),
];
3. Add Persistence Layer
Create migration for verification logs (ip_verifications) with fields like:
- ip
- source (web, api, web:bulk, etc.)
- verification_guid
- geo fields (country_code, city, latitude, etc.)
- raw_response
- error_message
- verified_at
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('ip_verifications', function (Blueprint $table): void {
$table->id();
$table->string('ip', 45);
$table->string('source')->default('web');
$table->string('verification_guid', 36)->nullable();
$table->boolean('is_valid')->default(false);
$table->string('type')->nullable();
$table->string('country_name')->nullable();
$table->string('country_code', 8)->nullable();
$table->string('region_name')->nullable();
$table->string('city')->nullable();
$table->decimal('latitude', 10, 7)->nullable();
$table->decimal('longitude', 10, 7)->nullable();
$table->string('connection_isp')->nullable();
$table->string('connection_type')->nullable();
$table->json('raw_response')->nullable();
$table->string('error_message')->nullable();
$table->timestamp('verified_at')->nullable();
$table->timestamps();
$table->index(['ip', 'created_at']);
$table->index(['source', 'created_at']);
$table->index('verification_guid');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('ip_verifications');
}
};
Then migrate:
php artisan migrate
Model: IpVerification.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class IpVerification extends Model
{
protected $fillable = [
'ip',
'source',
'verification_guid',
'is_valid',
'type',
'country_name',
'country_code',
'region_name',
'city',
'latitude',
'longitude',
'connection_isp',
'connection_type',
'raw_response',
'error_message',
'verified_at',
];
protected function casts(): array
{
return [
'is_valid' => 'boolean',
'latitude' => 'float',
'longitude' => 'float',
'raw_response' => 'array',
'verified_at' => 'datetime',
];
}
}
4. Build an Ipstack Service Layer
Create IpstackService.php to centralize all package usage:
<?php
namespace App\Services;
use Ipstack\Client\IpstackClient;
use Ipstack\Client\Options;
use Ipstack\Exception\IpstackException;
use Ipstack\Ipstack;
use Ipstack\Model\IpstackResult;
use RuntimeException;
class IpstackService
{
public function verify(string $ip): IpstackResult
{
return $this->run(fn (): IpstackResult => $this->client()->lookup($ip));
}
public function verifyRequester(): IpstackResult
{
return $this->run(fn (): IpstackResult => $this->client()->lookupRequester());
}
/**
* @param list<string> $ips
* @return list<IpstackResult>
*/
public function verifyBulk(array $ips): array
{
$client = $this->client();
try {
return $this->run(fn (): array => $client->lookupBulk($ips)->all());
} catch (RuntimeException $exception) {
if (! $this->shouldFallbackFromBulk($exception)) {
throw $exception;
}
}
// Free plans can reject bulk endpoints; fallback to sequential single lookups.
return $this->run(function () use ($client, $ips): array {
$results = [];
foreach ($ips as $ip) {
$results[] = $client->lookup($ip);
}
return $results;
});
}
public function verifyOptimized(string $ip, string $language = 'en'): IpstackResult
{
$enableAdvanced = (bool) config('ipstack.enable_advanced_options', false);
$fields = ['ip', 'type', 'country_name', 'country_code', 'region_name', 'city', 'latitude', 'longitude'];
if ($enableAdvanced) {
$fields[] = 'connection';
}
$options = Options::create()
->fields($fields)
->language($language);
if ($enableAdvanced) {
$options->security(true)->hostname(true);
}
return $this->run(fn (): IpstackResult => $this->client()->lookup($ip, $options));
}
private function client(): IpstackClient
{
$accessKey = (string) config('ipstack.access_key');
if ($accessKey === '') {
throw new RuntimeException('IPSTACK_ACCESS_KEY is not configured.');
}
return Ipstack::factory()
->withAccessKey($accessKey)
->withBaseUrl((string) config('ipstack.base_url', 'https://api.ipstack.com'))
->withTransport(new LaravelIpstackTransport((int) config('ipstack.timeout', 10)))
->build();
}
/**
* @template T
* @param callable():T $callback
* @return T
*/
private function run(callable $callback): mixed
{
try {
return $callback();
} catch (IpstackException $exception) {
throw new RuntimeException($exception->getMessage(), previous: $exception);
}
}
private function shouldFallbackFromBulk(RuntimeException $exception): bool
{
$message = strtolower($exception->getMessage());
return str_contains($message, 'http 403')
|| str_contains($message, 'batch')
|| str_contains($message, 'bulk');
}
}
Key implementation details:
- Use
Ipstack::factory()from your package. - Use a transport adapter (
LaravelIpstackTransport) so Laravel HTTP client can power package calls.
<?php namespace App\Services; use Illuminate\Support\Facades\Http; use Ipstack\Exception\InvalidResponseException; use Ipstack\Exception\TransportException; use Ipstack\Transport\TransportInterface; class LaravelIpstackTransport implements TransportInterface { public function __construct(private readonly int $timeout = 10) { } /** * @return array<string, mixed>|array<int, mixed> */ public function get(string $url, array $query): array { try { $response = Http::timeout($this->timeout)->get($url, $query); } catch (\Throwable $exception) { throw new TransportException($exception->getMessage(), previous: $exception); } if (! $response->successful()) { throw new TransportException('HTTP '.$response->status().' returned by upstream IP service.'); } $payload = $response->json(); if (! is_array($payload)) { throw new InvalidResponseException('Invalid JSON payload returned by upstream IP service.'); } return $payload; } } - For optimized mode:
- free-safe defaults (basic fields + language)
- only enable advanced flags when configured
- For bulk mode:
- try lookupBulk()
- fallback to sequential lookup() when upstream bulk is blocked (e.g. free-plan 403)
That fallback keeps the feature usable in lower-tier accounts.
5. Add GUID Traceability
In controllers, generate per-request IDs:
$verificationGuid = (new \Sujip\Guid\Guid())->create();
Persist this in each verification record and return it in API responses.
This gives you traceability across frontend, backend logs, and DB records.
6. Add Web + API Controllers
Web controller use cases
store()→ single lookuprequester()→ requester endpointoptimized()→ option-based lookupbulk()→ multi-IP processing
<?php
namespace App\Http\Controllers;
use App\Http\Requests\VerifyBulkIpRequest;
use App\Http\Requests\VerifyIpRequest;
use App\Http\Requests\VerifyOptimizedIpRequest;
use App\Models\IpVerification;
use App\Services\IpstackService;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
use Ipstack\Model\IpstackResult;
use RuntimeException;
use Sujip\Guid\Guid;
class IpVerificationController extends Controller
{
public function __construct(private readonly IpstackService $ipstackService) {}
public function index(): View
{
return view('ip-verifications.index', [
'recentVerifications' => IpVerification::latest()->limit(15)->get(),
]);
}
public function store(VerifyIpRequest $request): RedirectResponse
{
$ip = (string) ($request->validated()['ip'] ?? $request->ip());
return $this->handleSingle(fn() => $this->ipstackService->verify($ip), 'web');
}
public function requester(): RedirectResponse
{
return $this->handleSingle(fn() => $this->ipstackService->verifyRequester(), 'web:requester');
}
public function optimized(VerifyOptimizedIpRequest $request): RedirectResponse
{
$validated = $request->validated();
$ip = (string) ($validated['ip'] ?? $request->ip());
$language = (string) ($validated['language'] ?? 'en');
return $this->handleSingle(
fn() => $this->ipstackService->verifyOptimized($ip, $language),
'web:optimized'
);
}
public function bulk(VerifyBulkIpRequest $request): RedirectResponse
{
$verificationGuid = (new Guid())->create();
/** @var list<string> $ips */
$ips = array_values($request->validated()['ips']);
try {
$results = $this->ipstackService->verifyBulk($ips);
} catch (RuntimeException $exception) {
IpVerification::create([
'ip' => implode(',', $ips),
'source' => 'web:bulk',
'verification_guid' => $verificationGuid,
'is_valid' => false,
'error_message' => $exception->getMessage(),
]);
return back()->withErrors(['ip' => $exception->getMessage()])->withInput();
}
foreach ($results as $result) {
$this->persistResult($result, 'web:bulk', $verificationGuid);
}
return redirect()
->route('ip-verifications.index')
->with('status', 'Bulk verification completed for ' . count($results) . ' IP(s).');
}
/**
* @param callable():IpstackResult $callback
*/
private function handleSingle(callable $callback, string $source): RedirectResponse
{
$verificationGuid = (new Guid())->create();
try {
$result = $callback();
} catch (RuntimeException $exception) {
IpVerification::create([
'ip' => request()->ip() ?? 'unknown',
'source' => $source,
'verification_guid' => $verificationGuid,
'is_valid' => false,
'error_message' => $exception->getMessage(),
]);
return back()->withErrors(['ip' => $exception->getMessage()])->withInput();
}
$verification = $this->persistResult($result, $source, $verificationGuid);
return redirect()
->route('ip-verifications.index')
->with('verification_id', $verification->id)
->with('status', 'IP verification completed successfully.');
}
private function persistResult(IpstackResult $result, string $source, string $verificationGuid): IpVerification
{
return IpVerification::create([
'ip' => $result->ip,
'source' => $source,
'verification_guid' => $verificationGuid,
'is_valid' => true,
'type' => $result->type,
'country_name' => $result->country->name,
'country_code' => $result->country->code,
'region_name' => $result->region->name,
'city' => $result->city,
'latitude' => $result->latitude,
'longitude' => $result->longitude,
'connection_isp' => $result->connection?->isp,
'connection_type' => $result->connection?->organizationType,
'raw_response' => $result->raw(),
'verified_at' => now(),
]);
}
}
API controller use cases
- same 4 flows in JSON form
- include
verification_guidin success/error payloads
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\VerifyBulkIpRequest;
use App\Http\Requests\VerifyIpRequest;
use App\Http\Requests\VerifyOptimizedIpRequest;
use App\Models\IpVerification;
use App\Services\IpstackService;
use Illuminate\Http\JsonResponse;
use Ipstack\Model\IpstackResult;
use RuntimeException;
use Sujip\Guid\Guid;
class IpVerificationController extends Controller
{
public function __construct(private readonly IpstackService $ipstackService)
{
}
public function store(VerifyIpRequest $request): JsonResponse
{
$ip = (string) ($request->validated()['ip'] ?? $request->ip());
return $this->singleResponse(fn () => $this->ipstackService->verify($ip), 'api');
}
public function requester(): JsonResponse
{
return $this->singleResponse(fn () => $this->ipstackService->verifyRequester(), 'api:requester');
}
public function optimized(VerifyOptimizedIpRequest $request): JsonResponse
{
$validated = $request->validated();
$ip = (string) ($validated['ip'] ?? $request->ip());
$language = (string) ($validated['language'] ?? 'en');
return $this->singleResponse(
fn () => $this->ipstackService->verifyOptimized($ip, $language),
'api:optimized'
);
}
public function bulk(VerifyBulkIpRequest $request): JsonResponse
{
$verificationGuid = (new Guid())->create();
/** @var list<string> $ips */
$ips = array_values($request->validated()['ips']);
try {
$results = $this->ipstackService->verifyBulk($ips);
} catch (RuntimeException $exception) {
IpVerification::create([
'ip' => implode(',', $ips),
'source' => 'api:bulk',
'verification_guid' => $verificationGuid,
'is_valid' => false,
'error_message' => $exception->getMessage(),
]);
return response()->json([
'message' => $exception->getMessage(),
'verification_guid' => $verificationGuid,
], 422);
}
$data = [];
foreach ($results as $result) {
$verification = $this->persistResult($result, 'api:bulk', $verificationGuid);
$data[] = $this->toPayload($verification);
}
return response()->json([
'message' => 'Bulk verification completed successfully.',
'verification_guid' => $verificationGuid,
'count' => count($data),
'data' => $data,
]);
}
/**
* @param callable():IpstackResult $callback
*/
private function singleResponse(callable $callback, string $source): JsonResponse
{
$verificationGuid = (new Guid())->create();
try {
$result = $callback();
} catch (RuntimeException $exception) {
IpVerification::create([
'ip' => request()->ip() ?? 'unknown',
'source' => $source,
'verification_guid' => $verificationGuid,
'is_valid' => false,
'error_message' => $exception->getMessage(),
]);
return response()->json([
'message' => $exception->getMessage(),
'verification_guid' => $verificationGuid,
], 422);
}
$verification = $this->persistResult($result, $source, $verificationGuid);
return response()->json([
'message' => 'IP verification completed successfully.',
'verification_guid' => $verificationGuid,
'data' => $this->toPayload($verification),
]);
}
private function persistResult(IpstackResult $result, string $source, string $verificationGuid): IpVerification
{
return IpVerification::create([
'ip' => $result->ip,
'source' => $source,
'verification_guid' => $verificationGuid,
'is_valid' => true,
'type' => $result->type,
'country_name' => $result->country->name,
'country_code' => $result->country->code,
'region_name' => $result->region->name,
'city' => $result->city,
'latitude' => $result->latitude,
'longitude' => $result->longitude,
'connection_isp' => $result->connection?->isp,
'connection_type' => $result->connection?->organizationType,
'raw_response' => $result->raw(),
'verified_at' => now(),
]);
}
/**
* @return array<string, mixed>
*/
private function toPayload(IpVerification $verification): array
{
return [
'id' => $verification->id,
'ip' => $verification->ip,
'type' => $verification->type,
'country_name' => $verification->country_name,
'country_code' => $verification->country_code,
'region_name' => $verification->region_name,
'city' => $verification->city,
'latitude' => $verification->latitude,
'longitude' => $verification->longitude,
'connection_isp' => $verification->connection_isp,
'connection_type' => $verification->connection_type,
'verified_at' => optional($verification->verified_at)?->toIso8601String(),
];
}
}
7. Add Request Validation
Use dedicated request classes:
VerifyIpRequestfor single
<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class VerifyIpRequest extends FormRequest { /** * Determine if the user is authorized to make this request. */ public function authorize(): bool { return true; } /** * Get the validation rules that apply to the request. * * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string> */ public function rules(): array { return [ 'ip' => ['nullable', 'ip'], ]; } } VerifyOptimizedIpRequestfor optimized
<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class VerifyOptimizedIpRequest extends FormRequest { public function authorize(): bool { return true; } /** * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string> */ public function rules(): array { return [ 'ip' => ['nullable', 'ip'], 'language' => ['nullable', 'string', 'max:5'], ]; } } VerifyBulkIpRequestfor bulk
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class VerifyBulkIpRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$ips = $this->input('ips');
if (is_string($ips)) {
$parsed = preg_split('/[\s,]+/', trim($ips), -1, PREG_SPLIT_NO_EMPTY) ?: [];
$this->merge(['ips' => array_values(array_unique($parsed))]);
}
}
/**
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'ips' => ['required', 'array', 'min:1', 'max:50'],
'ips.*' => ['required', 'ip'],
];
}
}
For bulk, accept comma/newline input and normalize into an array, enforce max 50 IPs.
8. Register Routes
Web (web.php):
<?php
use App\Http\Controllers\IpVerificationController;
use Illuminate\Support\Facades\Route;
Route::get('/ip-verifications', [IpVerificationController::class, 'index'])
->name('ip-verifications.index');
Route::post('/ip-verifications', [IpVerificationController::class, 'store'])
->name('ip-verifications.store');
Route::post('/ip-verifications/requester', [IpVerificationController::class, 'requester'])
->name('ip-verifications.requester');
Route::post('/ip-verifications/optimized', [IpVerificationController::class, 'optimized'])
->name('ip-verifications.optimized');
Route::post('/ip-verifications/bulk', [IpVerificationController::class, 'bulk'])
->name('ip-verifications.bulk');
API (api.php):
<?php
use App\Http\Controllers\Api\IpVerificationController;
use Illuminate\Support\Facades\Route;
Route::post('/ip-verifications', [IpVerificationController::class, 'store'])
->name('api.ip-verifications.store');
Route::post('/ip-verifications/requester', [IpVerificationController::class, 'requester'])
->name('api.ip-verifications.requester');
Route::post('/ip-verifications/optimized', [IpVerificationController::class, 'optimized'])
->name('api.ip-verifications.optimized');
Route::post('/ip-verifications/bulk', [IpVerificationController::class, 'bulk'])
->name('api.ip-verifications.bulk');
9. Build a Practical Dashboard UI
In index.blade.php, add forms for all four actions:
- single input
- requester trigger button
- optimized form (ip + language)
- bulk textarea (comma/newline)
Show recent records table with:
- request GUID
- source
- status
- location
- ISP
- timestamp
This makes demos and QA straightforward.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IP Verification Dashboard</title>
@vite(['resources/css/app.css'])
<style>
body { font-family: ui-sans-serif, system-ui, sans-serif; margin: 0; background: #f4f7fb; color: #1a2533; }
.container { max-width: 1080px; margin: 32px auto; padding: 0 16px; }
.card { background: #fff; border: 1px solid #dbe4ef; border-radius: 12px; padding: 20px; margin-bottom: 18px; }
.title { margin: 0 0 10px; font-size: 26px; }
.muted { color: #5f6f83; }
.grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
label { display: block; font-size: 14px; margin-bottom: 6px; }
input, textarea { width: 100%; padding: 10px 12px; border: 1px solid #b9c7d8; border-radius: 8px; }
button { padding: 10px 14px; border: 0; border-radius: 8px; cursor: pointer; background: #0b63ce; color: #fff; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 10px 8px; border-top: 1px solid #e6edf5; font-size: 14px; }
.ok { color: #0a7d35; }
.err { color: #b42318; }
.flash { background: #e8f2ff; border: 1px solid #c6dcff; color: #08498f; padding: 10px 12px; border-radius: 8px; margin-bottom: 12px; }
.error { background: #ffeceb; border: 1px solid #ffc7c2; color: #982222; padding: 10px 12px; border-radius: 8px; margin-bottom: 12px; }
.forms { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
code { background: #eef3f8; padding: 1px 4px; border-radius: 4px; }
</style>
</head>
<body>
<div class="container">
<div class="card">
<h1 class="title">IP Verification Dashboard</h1>
<p class="muted">Use multiple optimized <code>sudiptpa/ipstack</code> flows: single lookup, requester lookup, option-based lookup, and bulk lookup (max 50 IPs).</p>
@if (session('status'))
<div class="flash">{{ session('status') }}</div>
@endif
@if ($errors->any())
<div class="error">{{ $errors->first() }}</div>
@endif
<div class="forms">
<form method="POST" action="{{ route('ip-verifications.store') }}" class="card" style="margin:0;">
@csrf
<h3 style="margin-top:0;">Single IP Lookup</h3>
<label for="ip">IP Address</label>
<input id="ip" name="ip" type="text" value="{{ old('ip') }}" placeholder="8.8.8.8" />
<div style="margin-top:12px;"><button type="submit">Verify IP</button></div>
</form>
<form method="POST" action="{{ route('ip-verifications.requester') }}" class="card" style="margin:0;">
@csrf
<h3 style="margin-top:0;">Requester Lookup</h3>
<p class="muted">Uses ipstack <code>/check</code> endpoint via package <code>lookupRequester()</code>.</p>
<div style="margin-top:12px;"><button type="submit">Verify Requester IP</button></div>
</form>
<form method="POST" action="{{ route('ip-verifications.optimized') }}" class="card" style="margin:0;">
@csrf
<h3 style="margin-top:0;">Optimized Lookup</h3>
<label for="optimized_ip">IP Address</label>
<input id="optimized_ip" name="ip" type="text" value="{{ old('ip') }}" placeholder="1.1.1.1" />
<label for="language" style="margin-top:8px;">Language (e.g. en, de)</label>
<input id="language" name="language" type="text" value="{{ old('language', 'en') }}" placeholder="en" />
<p class="muted" style="margin:8px 0 0;">Applies <code>fields</code>, <code>language</code>, <code>security</code>, <code>hostname</code> options.</p>
<div style="margin-top:12px;"><button type="submit">Run Optimized Lookup</button></div>
</form>
<form method="POST" action="{{ route('ip-verifications.bulk') }}" class="card" style="margin:0;">
@csrf
<h3 style="margin-top:0;">Bulk Lookup</h3>
<label for="ips">IPs (comma or newline separated, max 50)</label>
<textarea id="ips" name="ips" rows="4" placeholder="8.8.8.8, 1.1.1.1">{{ old('ips') }}</textarea>
<div style="margin-top:12px;"><button type="submit">Run Bulk Lookup</button></div>
</form>
</div>
</div>
<div class="card">
<h2 style="margin-top: 0;">Recent Verifications</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>Request GUID</th>
<th>Source</th>
<th>IP</th>
<th>Status</th>
<th>Location</th>
<th>ISP</th>
<th>Verified At</th>
</tr>
</thead>
<tbody>
@forelse ($recentVerifications as $item)
<tr>
<td>{{ $item->id }}</td>
<td>{{ $item->verification_guid ?: '-' }}</td>
<td>{{ strtoupper($item->source) }}</td>
<td>{{ $item->ip }}</td>
<td>@if($item->is_valid)<span class="ok">Success</span>@else<span class="err">Failed</span>@endif</td>
<td>{{ $item->city ?: '-' }} @if($item->country_code) ({{ $item->country_code }}) @endif</td>
<td>{{ $item->connection_isp ?: '-' }}</td>
<td>{{ optional($item->verified_at)->format('Y-m-d H:i:s') ?: '-' }}</td>
</tr>
@empty
<tr><td colspan="8" class="muted">No verifications yet.</td></tr>
@endforelse
</tbody>
</table>
</div>
<div class="card">
<h2 style="margin-top: 0;">API Examples</h2>
<p class="muted">Single: <code>POST /api/ip-verifications</code> body <code>{"ip":"8.8.8.8"}</code></p>
<p class="muted">Requester: <code>POST /api/ip-verifications/requester</code> body <code>{}</code></p>
<p class="muted">Optimized: <code>POST /api/ip-verifications/optimized</code> body <code>{"ip":"1.1.1.1","language":"en"}</code></p>
<p class="muted" style="margin-bottom:0;">Bulk: <code>POST /api/ip-verifications/bulk</code> body <code>{"ips":["8.8.8.8","1.1.1.1"]}</code></p>
</div>
</div>
</body>
</html>
10. Real-World Results from Local Validation
What worked well
- Full Laravel 12 integration across web + API
- Package APIs used directly in service layer
- GUID traceability in records/responses
- Free-plan hardening:
- optimized flow avoids paid-only options by default
- bulk flow gracefully falls back to single lookups on 403
Typical outcomes observed
- Single and requester lookups return expected mapped geo data
- Optimized lookups run successfully on free plan with safe config
- Bulk requests still complete even when bulk endpoint is restricted upstream
Production Notes
- Add rate limiting for public API endpoints.
- Consider queueing large bulk operations.
- Mask/retain only necessary raw response fields for compliance.
- Add alerting on repeated upstream failures.
- Enable advanced options only when account tier supports them.
Conclusion
This Laravel 12 implementation demonstrates a complete, real-world package integration:
sudiptpa/ipstackfor IP intelligencesudiptpa/guidfor request-level traceability- resilient handling for free-plan limitations
- web and API surfaces ready for practical use
Also Read: https://sujipthapa.com/blog/ipstack-php-wrapper-accurate-ip-geolocation-api-integration-for-developers-businesses
