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 (webapiweb:bulk, etc.)
  • verification_guid
  • geo fields (country_codecitylatitude, 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:

  1. Use Ipstack::factory() from your package.
  2. 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;
        }
    }
    ​


  3. For optimized mode:
    1. free-safe defaults (basic fields + language)
    2. only enable advanced flags when configured
  4. For bulk mode:
    1. try lookupBulk()
    2. 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 lookup
  • requester() → requester endpoint
  • optimized() → option-based lookup
  • bulk() → 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_guid in 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:

  • VerifyIpRequest for 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'],
            ];
        }
    }
    ​
  • VerifyOptimizedIpRequest for 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'],
            ];
        }
    }
    ​
  • VerifyBulkIpRequest for 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>

 

ipstack preveiw

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

  1. Add rate limiting for public API endpoints.
  2. Consider queueing large bulk operations.
  3. Mask/retain only necessary raw response fields for compliance.
  4. Add alerting on repeated upstream failures.
  5. Enable advanced options only when account tier supports them.

Conclusion

This Laravel 12 implementation demonstrates a complete, real-world package integration:

  • sudiptpa/ipstack for IP intelligence
  • sudiptpa/guid for 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