Back to Blog

How to Build a Link Tracking & Referral System in Laravel (Step-by-Step Guide)

Admin User

June 21, 2026
20
0

Introduction: 

Have you even vendored how many companies track affiliated link and referal trafic and the users click? 

Every time someone clicks on Amazon, Airbnb or other affilated marketing site a track a system record including: 

  • Who share the link
  • Who clicked the link
  • Get the Visitor IP address
  • Device Information
  • Country & Location 
  • Total Clicks
  • Conversion Statistics

In this tutorial, we'll build a complete Link Tracking & Referral System using Laravel.

By the end of this guide you'll learn:

  • Generate referral links
  • Track Every Click
  • Store Visitor Information Like  Name, location, ip etc
  • Cretae API's for analytics
  • Generate Referal Report
  • Building Scaleable Referral System 

How to Build a Link Tracking & Referral System in Laravel (Step-by-Step Guide)

Befor starting our project its my recemendation you must have the intermediate knowledge about the Laravel Php framework. This will help to you develop a scalable referral link system and user analytics dashboard. Below are the the step bye project guide to deelop our  referral link tracking system.

Step01: Create Laravel Php Latest Project:

For creating laravel project create Laravel Project folder inside your storate like D: or E: drive and open your terminal run below command for creating a new Laravel project.

composer create-project laravel/laravel referral-tracker 
cd referral-tracker 

To Run your servier enter below command

php artisan serve

Step 02: Configuration Dtabase:

After creating your project configuration the database. for this step open your xampp, Wamp or Lamp server if your using mysql database, in other case if you want to user postgresql you must opn PG admin and creation your database like referral_tracker. after creating database open your .eve file in your Laravel project root and enter the correct database name which you recently created.

DB_CONNECTION=mysql 
DB_HOST=127.0.0.1 
DB_PORT=3306 
DB_DATABASE=referral_tracker 
DB_USERNAME=root 
DB_PASSWORD=

after update your database info run your migration command 

php artisan migerate

this will migration the default migration tables like users, personal_access_token, migrations etc.

Step 03: Create Tracking Tables

For developing a scalable and professional affiliate marketing tracking we need to create a migrations so in this project we need to create two migrations (tables) one is Referral and other is Link Tracking. Open your terminal and run below command 

php artisan make:model Referral -m -c

The above command will create automatically referrals migration inside your database/migrations directory and model inside your App/Models/Referral.php and Controller in your App/Http/Controllers/ReferralControlelr. Note the -m and -c tag are use to create the migration and controller auto.

Update you referrrals migration and past below code

Schema::create('referrals', function (Blueprint $table) {
   $table->id();
  $table->foreignId('user_id')->constrained()->onDelete('cascade');
  $table->string('name')->nullable(); // Link name for dashboard
  $table->string('code')->unique();
  $table->string('destination_url');
  $table->integer('total_clicks')->default(0);
  $table->integer('unique_clicks')->default(0); // NEW: Unique clicks
  $table->integer('conversions')->default(0); // NEW: Conversions
  $table->boolean('is_active')->default(true); // NEW: Active status
  $table->timestamp('expires_at')->nullable(); // NEW: Expiration
  $table->timestamps();
});

Php artisan make:model ReferralClick -m -c

Open your referral_clicks migration and past below code

Schema::create('referral_clicks', function (Blueprint $table) {
    $table->id();
    $table->foreignId('referral_id')->constrained()->onDelete('cascade');
    $table->string('ip_address')->nullable();
    $table->text('user_agent')->nullable();
    $table->string('country')->nullable();
    $table->string('city')->nullable(); // NEW: City
    $table->string('region')->nullable(); // NEW: Region
    $table->string('device')->nullable();
    $table->string('browser')->nullable(); // NEW: Browser
    $table->string('os')->nullable(); // NEW: Operating System
    $table->string('referer_url')->nullable(); // NEW: Where they came from
    $table->string('utm_source')->nullable(); // NEW: UTM Source
    $table->string('utm_medium')->nullable(); // NEW: UTM Medium
    $table->string('utm_campaign')->nullable(); // NEW: UTM Campaign
    $table->string('utm_term')->nullable(); // NEW: UTM Term
    $table->string('utm_content')->nullable(); // NEW: UTM Content
    $table->boolean('is_unique')->default(true); // NEW: Unique click flag
    $table->boolean('is_converted')->default(false); // NEW: Converted flag
    $table->timestamps();
});

php artisan migrate

Add the Relationship in User model

// app/Models/User.php
public function referrals()
{
    return $this->hasMany(Referral::class);
}

public function referralClicks()
{
    return $this->hasManyThrough(ReferralClick::class, Referral::class);
}

Step 04: Update Models 

Open your Referral model and past below cde

// app/Models/Referral.php
class Referral extends Model
{
    protected $fillable = [
        'user_id',
        'name',
        'code',
'destination_url',
'total_clicks',
'unique_clicks',
        'conversions',
        'is_active',
        'expires_at'
    ];

    protected $casts = [
        'expires_at' => 'datetime',
        'is_active' => 'boolean'
    ];

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function clicks()
    {
        return $this->hasMany(ReferralClick::class);
    }

    public function uniqueClicks()
    {
        return $this->hasMany(ReferralClick::class)->where('is_unique', true);
    }

    public function conversions()
    {
        return $this->hasMany(ReferralClick::class)->where('is_converted', true);
    }

    public function getClickRateAttribute()
    {
        return $this->unique_clicks > 0 
            ? round(($this->conversions / $this->unique_clicks) * 100, 2) 
            : 0;
    }

    public function isExpired()
    {
        return $this->expires_at && $this->expires_at->isPast();
    }

    // Generate unique code
    public static function generateUniqueCode()
    {
        do {
            $code = Str::random(10);
        } while (self::where('code', $code)->exists());
        
        return $code;
    }
}

Update ReferralClick Model open this and past below code

// app/Models/ReferralClick.php
class ReferralClick extends Model
{
    protected $fillable = [
        'referral_id',
        'ip_address',
        'user_agent',
        'country',
        'city',
        'region',
        'device',
        'browser',
        'os',
        'referer_url',
        'utm_source',
        'utm_medium',
'utm_campaign',
        'utm_term',
        'utm_content',
        'is_unique',
        'is_converted'
    ];

    protected $casts = [
        'is_unique' => 'boolean',
        'is_converted' => 'boolean'
    ];

    public function referral()
    {
        return $this->belongsTo(Referral::class);
    }

    // Scope for today's clicks
    public function scopeToday($query)
    {
        return $query->whereDate('created_at', today());
    }

    // Scope for unique clicks
    public function scopeUnique($query)
    {
        return $query->where('is_unique', true);
    }

    // Scope for conversions
    public function scopeConverted($query)
    {
        return $query->where('is_converted', true);
    }
}

Step 05: Update ReferralController

// app/Http/Controllers/ReferralController.php
<?php

namespace App\Http\Controllers;

use App\Models\Referral;
use App\Models\ReferralClick;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
use Stevebauman\Location\Facades\Location;
use SimpleSoftwareIO\QrCode\Facades\QrCode;
use Carbon\Carbon;

class ReferralController extends Controller
{
    public function __construct()
    {
$this->middleware('auth')->except(['track', 'analytics']);
    }

    // ============ DASHBOARD ============
    public function dashboard()
    {
        $user = auth()->user();
        
        $stats = [
'total_links' => $user->referrals()->count(),
'total_clicks' => $user->referralClicks()->count(),
'unique_clicks' => $user->referralClicks()->unique('ip_address')->count(),
'total_conversions' => $user->referralClicks()->where('is_converted', true)->count(),
'today_clicks' => $user->referralClicks()->today()->count(),
'click_rate' => $user->referralClicks()->count() > 0 
                ? round(($user->referralClicks()->where('is_converted', true)->count() / 
$user->referralClicks()->count()) * 100, 2)
                : 0
        ];

        // Recent clicks with pagination
        $recentClicks = $user->referralClicks()
->with('referral')
->latest()
->limit(10)
->get();

        // Chart data for last 7 days
        $chartData = $this->getChartData($user);

        // Top performing links
        $topLinks = $user->referrals()
->orderBy('total_clicks', 'desc')
->limit(5)
->get();

        return view('referrals.dashboard', compact(
            'stats', 
'recentClicks', 
'chartData', 
            'topLinks'
        ));
    }

    // ============ CREATE REFERRAL LINK ============
    public function create(Request $request)
    {
$request->validate([
            'name' => 'required|string|max:255',
'destination_url' => 'required|url',
'expires_at' => 'nullable|date|after:now'
        ]);

        $referral = Referral::create([
            'user_id' => auth()->id(),
            'name' => $request->name,
            'code' => Referral::generateUniqueCode(),
'destination_url' => $request->destination_url,
'expires_at' => $request->expires_at
        ]);

        return response()->json([
            'success' => true,
            'message' => 'Referral link created successfully!',
            'referral' => $referral,
'tracking_url' => url('/r/' . $referral->code)
        ]);
    }

    // ============ TRACK CLICKS (Complete) ============
    public function track($code, Request $request)
    {
        $referral = Referral::where('code', $code)
->where('is_active', true)
->firstOrFail();

        // Check if link is expired
        if ($referral->isExpired()) {
            abort(410, 'This referral link has expired.');
        }

        $ip = $request->ip();
        $userAgent = $request->userAgent();

        // Check for unique click (same IP within 24 hours)
        $existingClick = ReferralClick::where('referral_id', $referral->id)
->where('ip_address', $ip)
->where('created_at', '>=', Carbon::now()->subHours(24))
->exists();

        $isUnique = !$existingClick;

        // Get location data
        $locationData = $this->getLocationData($ip);

        // Detect device, browser, OS
        $deviceInfo = $this->getDeviceInfo($userAgent);

        // Parse UTM parameters
        $utmData = $this->parseUtmParameters($request);

        // Create click record
        $click = ReferralClick::create([
'referral_id' => $referral->id,
'ip_address' => $ip,
'user_agent' => $userAgent,
            'country' => $locationData['country'] ?? null,
            'city' => $locationData['city'] ?? null,
            'region' => $locationData['region'] ?? null,
            'device' => $deviceInfo['device'],
            'browser' => $deviceInfo['browser'],
            'os' => $deviceInfo['os'],
'referer_url' => $request->headers->get('referer'),
'utm_source' => $utmData['utm_source'],
'utm_medium' => $utmData['utm_medium'],
'utm_campaign' => $utmData['utm_campaign'],
            'utm_term' => $utmData['utm_term'],
'utm_content' => $utmData['utm_content'],
'is_unique' => $isUnique
        ]);

        // Increment total and unique clicks
$referral->increment('total_clicks');
        if ($isUnique) {
$referral->increment('unique_clicks');
        }

        // Store click in session for conversion tracking
session(['last_referral_click_id' => $click->id]);

        return redirect($referral->destination_url);
    }

    // ============ CONVERSION TRACKING ============
    public function trackConversion(Request $request)
    {
        $clickId = session('last_referral_click_id');
        
        if ($clickId) {
            $click = ReferralClick::find($clickId);
            if ($click && !$click->is_converted) {
$click->update(['is_converted' => true]);
$click->referral->increment('conversions');
                
                return response()->json([
'success' => true,
'message' => 'Conversion tracked successfully!'
                ]);
            }
        }

        return response()->json([
            'success' => false,
            'message' => 'No referral click found to track.'
        ]);
    }

    // ============ ANALYTICS (Enhanced) ============
    public function analytics($id)
    {
        $referral = Referral::with(['user', 'clicks' => function($query) {
$query->latest()->limit(100);
}])->findOrFail($id);

        // Ensure user owns the referral
        if ($referral->user_id !== auth()->id()) {
abort(403);
        }

        // Advanced analytics
        $analytics = [
            'referral' => $referral,
'total_clicks' => $referral->total_clicks,
'unique_clicks' => $referral->unique_clicks,
'conversions' => $referral->conversions,
'conversion_rate' => $referral->click_rate,
            
            // Device breakdown
            'devices' => $referral->clicks()
->select('device', DB::raw('count(*) as total'))
->groupBy('device')
->get(),
            
            // Country breakdown
'countries' => $referral->clicks()
->select('country', DB::raw('count(*) as total'))
->whereNotNull('country')
->groupBy('country')
->get(),
            
            // Daily clicks last 30 days
'daily_clicks' => $referral->clicks()
->select(DB::raw('DATE(created_at) as date'), DB::raw('count(*) as total'))
->where('created_at', '>=', Carbon::now()->subDays(30))
->groupBy('date')
->orderBy('date')
->get(),
            
            // UTM source breakdown
'utm_sources' => $referral->clicks()
->select('utm_source', DB::raw('count(*) as total'))
->whereNotNull('utm_source')
->groupBy('utm_source')
->get(),
            
            // Recent clicks
'recent_clicks' => $referral->clicks()
->latest()
->limit(50)
->get()
        ];

        return response()->json($analytics);
    }

    // ============ UPDATE REFERRAL LINK ============
    public function update(Request $request, $id)
    {
        $referral = Referral::where('user_id', auth()->id())
->findOrFail($id);

$request->validate([
            'name' => 'sometimes|string|max:255',
'destination_url' => 'sometimes|url',
'is_active' => 'sometimes|boolean',
'expires_at' => 'nullable|date|after:now'
        ]);

$referral->update($request->all());

        return response()->json([
            'success' => true,
            'message' => 'Referral link updated successfully!',
            'referral' => $referral
        ]);
    }

    // ============ DELETE REFERRAL LINK ============
    public function delete($id)
    {
        $referral = Referral::where('user_id', auth()->id())
->findOrFail($id);

        // Optionally delete associated clicks
$referral->clicks()->delete();
$referral->delete();

        return response()->json([
            'success' => true,
            'message' => 'Referral link deleted successfully!'
        ]);
    }

    // ============ EXPORT REPORTS ============
    public function export($id)
    {
        $referral = Referral::where('user_id', auth()->id())
->with('clicks')
->findOrFail($id);

        $fileName = "referral_{$referral->code}_clicks.csv";
        
        $headers = [
'Content-Type' => 'text/csv',
'Content-Disposition' => "attachment; filename=\"$fileName\"",
        ];

        $callback = function() use ($referral) {
            $file = fopen('php://output', 'w');
            
            // Add headers
fputcsv($file, [
'Date', 'IP Address', 'Country', 'City', 
'Device', 'Browser', 'OS', 'UTM Source', 
'Unique Click', 'Converted'
            ]);

            // Add rows
            foreach ($referral->clicks as $click) {
fputcsv($file, [
$click->created_at,
$click->ip_address,
$click->country,
$click->city,
$click->device,
$click->browser,
$click->os,
$click->utm_source,
$click->is_unique ? 'Yes' : 'No',
$click->is_converted ? 'Yes' : 'No'
                ]);
            }

fclose($file);
        };

        return response()->stream($callback, 200, $headers);
    }

    // ============ GENERATE QR CODE ============
    public function generateQR($id)
    {
        $referral = Referral::where('user_id', auth()->id())
->findOrFail($id);

        $url = url('/r/' . $referral->code);
        
        // Generate QR code as base64
        $qrCode = base64_encode(
\QrCode::format('png')
->size(300)
->errorCorrection('H')
->generate($url)
        );

        return response()->json([
            'qr_code' => $qrCode,
            'url' => $url
        ]);
    }

    // ============ HELPER METHODS ============

    private function getChartData($user)
    {
        $data = [];
        for ($i = 6; $i >= 0; $i--) {
            $date = Carbon::now()->subDays($i);
$data['labels'][] = $date->format('D');
$data['clicks'][] = $user->referralClicks()
->whereDate('created_at', $date)
->count();
        }
        return $data;
    }

    private function getLocationData($ip)
    {
        try {
            $location = Location::get($ip);
            if ($location) {
                return [
'country' => $location->countryName,
'city' => $location->cityName,
'region' => $location->regionName
                ];
            }
        } catch (\Exception $e) {
            // Fallback to IP API or ignore
        }
        return ['country' => null, 'city' => null, 'region' => null];
    }

    private function getDeviceInfo($userAgent)
    {
        // Use a package like jenssegers/agent
        $agent = new \Jenssegers\Agent\Agent();
$agent->setUserAgent($userAgent);

        return [
            'device' => $agent->device() ?: 'Desktop',
            'browser' => $agent->browser(),
            'os' => $agent->platform()
        ];
    }

    private function parseUtmParameters($request)
    {
        return [
'utm_source' => $request->query('utm_source'),
'utm_medium' => $request->query('utm_medium'),
'utm_campaign' => $request->query('utm_campaign'),
            'utm_term' => $request->query('utm_term'),
'utm_content' => $request->query('utm_content')
        ];
    }
}

Step 06: Setup Route

Open your api.php or web.php inside Route folder and past below routes

// routes/api.php
Route::middleware(['auth'])->group(function () {
    // Dashboard
Route::get('/dashboard', [ReferralController::class, 'dashboard'])
->name('dashboard');
    
    // Referral management
Route::post('/referrals/create', [ReferralController::class, 'create'])
->name('referrals.create');
    
Route::put('/referrals/{id}', [ReferralController::class, 'update'])
->name('referrals.update');
    
Route::delete('/referrals/{id}', [ReferralController::class, 'delete'])
->name('referrals.delete');
    
    // Analytics
Route::get('/analytics/{id}', [ReferralController::class, 'analytics'])
->name('referrals.analytics');
    
    // Export
Route::get('/referrals/{id}/export', [ReferralController::class, 'export'])
->name('referrals.export');
    
    // QR Code
Route::get('/referrals/{id}/qr', [ReferralController::class, 'generateQR'])
->name('referrals.qr');
});

// Public tracking routes
Route::get('/r/{code}', [ReferralController::class, 'track'])
->name('referrals.track');

// Conversion tracking
Route::post('/track-conversion', [ReferralController::class, 'trackConversion'])
->name('track.conversion');

Step 07: TrackReferral Middleware 

Intercepts incoming requests to automatically capture and persist referral codes (?ref=) and standard UTM marketing parameters (utm_source, utm_medium, etc.) into the user's session. This ensures tracking data is preserved across pages until a user completes an action, such as registering or making a purchase.

Php artisan make:middleware TrackReferral

Open this middleware inside App/Http/Middleware/ TrackReferral.php and past below code

// app/Http/Middleware/TrackReferral.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class TrackReferral
{
    public function handle(Request $request, Closure $next)
    {
        // Store referral code in session if present
        if ($request->has('ref')) {
session(['referral_code' => $request->ref]);
        }

        // Store UTM parameters
        $utmParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
        foreach ($utmParams as $param) {
            if ($request->has($param)) {
session([$param => $request->$param]);
            }
        }

        return $next($request);
    }
}

Step 7: Install Required Packages

composer require stevebauman/location
composer require jenssegers/agent
composer require simplesoftwareio/qr-code
composer require barryvdh/laravel-dompdf
composer require pusher/pusher-php-server

Conclusion

Building a custom Link Tracking & Referral System gives you complete control over your application's marketing analytics without relying on heavy, third-party scripts.

By leveraging Laravel's core features, you have built a system that:

    • Dynamically generates clean, unique referral codes.
    • Captures visitor footprints (devices, IP locations, and HTTP referrers) in real time.
    • Maintains tracking states seamlessly across pages using custom middleware to catch UTM parameters and affiliate tokens.
    • Closes the loop by attributing new user registrations directly to their original traffic source.
How to Build a Link Tracking & Referral System in Laravel (Step-by-Step Guide)