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.

