diff --git a/database/migrations/2026_06_02_000007_add_retargeting_pixels_and_webhooks_to_short_urls_table.php b/database/migrations/2026_06_02_000007_add_retargeting_pixels_and_webhooks_to_short_urls_table.php
new file mode 100644
index 0000000..a676222
--- /dev/null
+++ b/database/migrations/2026_06_02_000007_add_retargeting_pixels_and_webhooks_to_short_urls_table.php
@@ -0,0 +1,36 @@
+string('pixel_meta_id', 100)->nullable();
+ $table->string('pixel_google_id', 100)->nullable();
+ $table->string('pixel_linkedin_id', 100)->nullable();
+ $table->string('webhook_url', 2048)->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('short_urls', function (Blueprint $table) {
+ $table->dropColumn([
+ 'pixel_meta_id',
+ 'pixel_google_id',
+ 'pixel_linkedin_id',
+ 'webhook_url',
+ ]);
+ });
+ }
+};
diff --git a/resources/lang/en/default.php b/resources/lang/en/default.php
index 7b98e6e..c5b92de 100644
--- a/resources/lang/en/default.php
+++ b/resources/lang/en/default.php
@@ -338,4 +338,33 @@ return [
'warning_description' => 'You are leaving this secure portal and being redirected to an external target link. Please ensure you trust the address below:',
'warning_btn_continue' => 'Continue to Destination',
'warning_btn_back' => 'Go Back',
+
+ // Marketing & API Tab
+ 'tab_marketing' => 'Marketing & API',
+ 'marketing_pixels_title' => 'Retargeting Pixels (Client-Side)',
+ 'marketing_pixels_desc' => 'Add ad tracking scripts to capture visitor cookies and build remarketing lists.',
+ 'pixel_meta' => 'Meta Pixel ID',
+ 'pixel_google' => 'Google Tag / GA4 ID',
+ 'pixel_linkedin' => 'LinkedIn Partner ID',
+ 'marketing_webhooks_title' => 'Link Webhook Integration',
+ 'marketing_webhooks_desc' => 'Configure a custom destination URL to send a real-time HTTP POST notification on each click.',
+ 'webhook_url' => 'Dedicated Webhook URL',
+
+ // Settings tab additions
+ 'settings_tab_developer' => 'API & Webhooks',
+ 'settings_section_global_webhook' => 'Global Webhook Configuration',
+ 'settings_global_webhook_url' => 'Global Webhook URL',
+ 'settings_global_webhook_url_helper' => 'Destination URL to dispatch event payloads asynchronously in the background for all links.',
+ 'settings_webhook_events' => 'Monitored Webhook Events',
+ 'settings_webhook_events_helper' => 'Select which events should trigger a POST notification.',
+ 'webhook_event_visited' => 'Link Visited (Click)',
+ 'webhook_event_created' => 'Link Created',
+ 'webhook_event_expired' => 'Link Expired (Date)',
+ 'webhook_event_limit_reached' => 'Link Click Limit Reached',
+ 'settings_section_api_keys' => 'Developer API Keys',
+ 'settings_api_keys_description' => 'Manage API keys to integrate with external systems (e.g. CRM, Zapier, Make). Authenticate requests using the X-Api-Key header.',
+ 'settings_api_keys' => 'Active API Keys',
+ 'api_key_name' => 'Key Name (Description)',
+ 'api_key' => 'API Key',
+ 'active' => 'Active',
];
diff --git a/resources/lang/pl/default.php b/resources/lang/pl/default.php
index 28f8061..d0cb416 100644
--- a/resources/lang/pl/default.php
+++ b/resources/lang/pl/default.php
@@ -339,4 +339,33 @@ return [
'warning_description' => 'Opuszczasz ten bezpieczny portal i zostajesz przekierowany na zewnętrzny link docelowy. Upewnij się, że ufasz poniższemu adresowi:',
'warning_btn_continue' => 'Kontynuuj do celu',
'warning_btn_back' => 'Wróć',
+
+ // Marketing & API Tab
+ 'tab_marketing' => 'Marketing i API',
+ 'marketing_pixels_title' => 'Piksele retargetingowe (Client-Side)',
+ 'marketing_pixels_desc' => 'Dodaj skrypty śledzące, aby dołączać ciasteczka odwiedzających i budować grupy remarketingowe reklam.',
+ 'pixel_meta' => 'Meta Pixel ID',
+ 'pixel_google' => 'Google Tag / GA4 ID',
+ 'pixel_linkedin' => 'LinkedIn Partner ID',
+ 'marketing_webhooks_title' => 'Integracja Webhook linku',
+ 'marketing_webhooks_desc' => 'Skonfiguruj niestandardowy adres URL, pod który wysłane zostanie natychmiastowe powiadomienie HTTP POST po kliknięciu.',
+ 'webhook_url' => 'Dedykowany Webhook URL',
+
+ // Settings tab additions
+ 'settings_tab_developer' => 'API i Webhooki',
+ 'settings_section_global_webhook' => 'Konfiguracja globalnego Webhooka',
+ 'settings_global_webhook_url' => 'Globalny Webhook URL',
+ 'settings_global_webhook_url_helper' => 'Adres URL, pod który wysyłane będą w tle powiadomienia o zdarzeniach ze wszystkich linków.',
+ 'settings_webhook_events' => 'Monitorowane zdarzenia Webhooka',
+ 'settings_webhook_events_helper' => 'Wybierz zdarzenia, które powinny wyzwalać wysłanie powiadomienia POST.',
+ 'webhook_event_visited' => 'Odwiedziny linku (Wizyta)',
+ 'webhook_event_created' => 'Utworzenie linku',
+ 'webhook_event_expired' => 'Wygaśnięcie linku (Data)',
+ 'webhook_event_limit_reached' => 'Osiągnięcie limitu kliknięć',
+ 'settings_section_api_keys' => 'Klucze API Dewelopera',
+ 'settings_api_keys_description' => 'Zarządzaj kluczami API do integracji z zewnętrznymi systemami (np. CRM, Zapier, Make). Autoryzacja odbywa się za pomocą nagłówka X-Api-Key.',
+ 'settings_api_keys' => 'Aktywne klucze API',
+ 'api_key_name' => 'Nazwa klucza (Opis)',
+ 'api_key' => 'Klucz API',
+ 'active' => 'Aktywny',
];
diff --git a/resources/views/pixel-loading.blade.php b/resources/views/pixel-loading.blade.php
new file mode 100644
index 0000000..7d8561f
--- /dev/null
+++ b/resources/views/pixel-loading.blade.php
@@ -0,0 +1,160 @@
+
+
+
+
+
+ Connecting...
+
+
+
+
+
+
+
+ @if(!empty($pixelMetaId))
+
+
+ @endif
+
+
+ @if(!empty($pixelGoogleId))
+
+
+ @endif
+
+
+ @if(!empty($pixelLinkedinId))
+
+
+ @endif
+
+
+
+
+
+
+
+
Connecting Safely
+
Securing connection & forwarding you now...
+
+
+
+
+
+
diff --git a/routes/web.php b/routes/web.php
index 6563d82..f502a40 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -11,3 +11,11 @@ Route::match(
->name('short-url.redirect')
->where('key', '[a-zA-Z0-9_-]+')
->middleware(config('filament-short-url.middleware', ['web', 'throttle:120,1']));
+
+Route::prefix('api/short-url')
+ ->middleware([\Bjanczak\FilamentShortUrl\Http\Middleware\AuthenticateShortUrlApi::class])
+ ->group(function () {
+ Route::get('links', [\Bjanczak\FilamentShortUrl\Http\Controllers\ShortUrlApiController::class, 'index']);
+ Route::post('links', [\Bjanczak\FilamentShortUrl\Http\Controllers\ShortUrlApiController::class, 'store']);
+ Route::delete('links/{id}', [\Bjanczak\FilamentShortUrl\Http\Controllers\ShortUrlApiController::class, 'destroy']);
+ });
diff --git a/src/Filament/Resources/ShortUrlResource/Pages/ShortUrlSettingsPage.php b/src/Filament/Resources/ShortUrlResource/Pages/ShortUrlSettingsPage.php
index 9075fec..3c3b475 100644
--- a/src/Filament/Resources/ShortUrlResource/Pages/ShortUrlSettingsPage.php
+++ b/src/Filament/Resources/ShortUrlResource/Pages/ShortUrlSettingsPage.php
@@ -74,6 +74,9 @@ class ShortUrlSettingsPage extends Page implements HasForms
'qr_gradient_from' => $mgr->get('qr_gradient_from', '#4f46e5'),
'qr_gradient_to' => $mgr->get('qr_gradient_to', '#06b6d4'),
'qr_gradient_type' => $mgr->get('qr_gradient_type', 'linear'),
+ 'global_webhook_url' => $mgr->get('global_webhook_url'),
+ 'webhook_events' => $mgr->get('webhook_events', ['visited']),
+ 'api_keys' => $mgr->get('api_keys', []),
]);
}
@@ -537,6 +540,56 @@ class ShortUrlSettingsPage extends Page implements HasForms
->visible(fn (Get $get): bool => (bool) $get('qr_gradient_enabled')),
]),
]),
+
+ // ── Developer API & Webhooks ────────────────────────
+ Tab::make(__('filament-short-url::default.settings_tab_developer'))
+ ->icon('heroicon-o-cpu-chip')
+ ->schema([
+ Section::make(__('filament-short-url::default.settings_section_global_webhook'))
+ ->schema([
+ TextInput::make('global_webhook_url')
+ ->label(__('filament-short-url::default.settings_global_webhook_url'))
+ ->helperText(__('filament-short-url::default.settings_global_webhook_url_helper'))
+ ->url()
+ ->nullable()
+ ->columnSpanFull(),
+
+ Select::make('webhook_events')
+ ->label(__('filament-short-url::default.settings_webhook_events'))
+ ->helperText(__('filament-short-url::default.settings_webhook_events_helper'))
+ ->multiple()
+ ->options([
+ 'visited' => __('filament-short-url::default.webhook_event_visited'),
+ 'created' => __('filament-short-url::default.webhook_event_created'),
+ 'expired' => __('filament-short-url::default.webhook_event_expired'),
+ 'limit_reached' => __('filament-short-url::default.webhook_event_limit_reached'),
+ ])
+ ->default(['visited'])
+ ->columnSpanFull(),
+ ]),
+
+ Section::make(__('filament-short-url::default.settings_section_api_keys'))
+ ->description(__('filament-short-url::default.settings_api_keys_description'))
+ ->schema([
+ \Filament\Forms\Components\Repeater::make('api_keys')
+ ->label(__('filament-short-url::default.settings_api_keys'))
+ ->schema([
+ TextInput::make('name')
+ ->label(__('filament-short-url::default.api_key_name'))
+ ->required(),
+ TextInput::make('key')
+ ->label(__('filament-short-url::default.api_key'))
+ ->disabled()
+ ->dehydrated()
+ ->default(fn () => 'sh_key_'.bin2hex(random_bytes(16))),
+ Toggle::make('is_active')
+ ->label(__('filament-short-url::default.active'))
+ ->default(true),
+ ])
+ ->columns(3)
+ ->default([]),
+ ]),
+ ]),
]),
])
->statePath('data');
diff --git a/src/Filament/Resources/ShortUrlResource/Schemas/ShortUrlForm.php b/src/Filament/Resources/ShortUrlResource/Schemas/ShortUrlForm.php
index 508db89..c3cb944 100644
--- a/src/Filament/Resources/ShortUrlResource/Schemas/ShortUrlForm.php
+++ b/src/Filament/Resources/ShortUrlResource/Schemas/ShortUrlForm.php
@@ -27,6 +27,7 @@ class ShortUrlForm
static::linkTab(),
static::targetingTab(),
static::trackingTab(),
+ static::marketingTab(),
static::qrDesignTab(),
])->columnSpanFull(),
]);
@@ -477,4 +478,43 @@ class ShortUrlForm
]),
]);
}
+
+ private static function marketingTab(): Tab
+ {
+ return Tab::make(__('filament-short-url::default.tab_marketing'))
+ ->icon('heroicon-o-megaphone')
+ ->schema([
+ Section::make(__('filament-short-url::default.marketing_pixels_title'))
+ ->description(__('filament-short-url::default.marketing_pixels_desc'))
+ ->schema([
+ TextInput::make('pixel_meta_id')
+ ->label(__('filament-short-url::default.pixel_meta'))
+ ->placeholder('e.g., 1234567890')
+ ->maxLength(100)
+ ->nullable(),
+ TextInput::make('pixel_google_id')
+ ->label(__('filament-short-url::default.pixel_google'))
+ ->placeholder('e.g., G-XXXXXXXXXX or AW-XXXXXXXXXX')
+ ->maxLength(100)
+ ->nullable(),
+ TextInput::make('pixel_linkedin_id')
+ ->label(__('filament-short-url::default.pixel_linkedin'))
+ ->placeholder('e.g., 1234567')
+ ->maxLength(100)
+ ->nullable(),
+ ])->columns(3),
+
+ Section::make(__('filament-short-url::default.marketing_webhooks_title'))
+ ->description(__('filament-short-url::default.marketing_webhooks_desc'))
+ ->schema([
+ TextInput::make('webhook_url')
+ ->label(__('filament-short-url::default.webhook_url'))
+ ->placeholder('https://api.yourcrm.com/webhooks/clicks')
+ ->url()
+ ->maxLength(2048)
+ ->nullable()
+ ->columnSpanFull(),
+ ]),
+ ]);
+ }
}
diff --git a/src/FilamentShortUrlServiceProvider.php b/src/FilamentShortUrlServiceProvider.php
index 6908043..45925dd 100644
--- a/src/FilamentShortUrlServiceProvider.php
+++ b/src/FilamentShortUrlServiceProvider.php
@@ -32,6 +32,7 @@ class FilamentShortUrlServiceProvider extends PackageServiceProvider
'2026_06_01_000004_add_targeting_and_security_to_short_urls_table',
'2026_06_01_000005_create_short_url_daily_stats_table',
'2026_06_02_000006_add_max_visits_and_expiration_redirect_to_short_urls_table',
+ '2026_06_02_000007_add_retargeting_pixels_and_webhooks_to_short_urls_table',
])
->hasCommands([
SyncBufferedCountersCommand::class,
diff --git a/src/Http/Controllers/ShortUrlApiController.php b/src/Http/Controllers/ShortUrlApiController.php
new file mode 100644
index 0000000..dc06871
--- /dev/null
+++ b/src/Http/Controllers/ShortUrlApiController.php
@@ -0,0 +1,137 @@
+paginate(30);
+
+ $transformed = $links->getCollection()->map(fn ($link) => $this->transformLink($link));
+
+ return response()->json([
+ 'data' => $transformed,
+ 'meta' => [
+ 'current_page' => $links->currentPage(),
+ 'last_page' => $links->lastPage(),
+ 'per_page' => $links->perPage(),
+ 'total' => $links->total(),
+ ],
+ ]);
+ }
+
+ /**
+ * Store a newly created short URL.
+ */
+ public function store(Request $request): JsonResponse
+ {
+ $validated = $request->validate([
+ 'destination_url' => 'required|url|max:2048',
+ 'url_key' => 'nullable|string|alpha_dash|max:50|unique:short_urls,url_key',
+ 'notes' => 'nullable|string|max:1000',
+ 'is_enabled' => 'nullable|boolean',
+ 'redirect_status_code' => 'nullable|integer|in:301,302',
+ 'single_use' => 'nullable|boolean',
+ 'forward_query_params' => 'nullable|boolean',
+ 'max_visits' => 'nullable|integer|min:1',
+ 'expiration_redirect_url' => 'nullable|url|max:2048',
+ 'activated_at' => 'nullable|date',
+ 'expires_at' => 'nullable|date',
+ 'pixel_meta_id' => 'nullable|string|max:100',
+ 'pixel_google_id' => 'nullable|string|max:100',
+ 'pixel_linkedin_id' => 'nullable|string|max:100',
+ 'webhook_url' => 'nullable|url|max:2048',
+ ]);
+
+ $shortUrl = $this->service->create($validated);
+
+ // Fire 'created' webhook if active
+ $this->dispatchCreatedWebhook($shortUrl);
+
+ return response()->json([
+ 'message' => 'Short URL created successfully.',
+ 'data' => $this->transformLink($shortUrl),
+ ], 201);
+ }
+
+ /**
+ * Remove the specified short URL.
+ */
+ public function destroy(int $id): JsonResponse
+ {
+ $shortUrl = ShortUrl::findOrFail($id);
+ $shortUrl->delete();
+
+ return response()->json([
+ 'message' => 'Short URL deleted successfully.',
+ ]);
+ }
+
+ /**
+ * Transform a ShortUrl model to API response array.
+ */
+ private function transformLink(ShortUrl $link): array
+ {
+ return [
+ 'id' => $link->id,
+ 'destination_url' => $link->destination_url,
+ 'url_key' => $link->url_key,
+ 'short_url' => $link->getShortUrl(),
+ 'is_enabled' => (bool) $link->is_enabled,
+ 'redirect_status_code' => (int) $link->redirect_status_code,
+ 'total_visits' => (int) $link->total_visits,
+ 'unique_visits' => (int) $link->unique_visits,
+ 'max_visits' => $link->max_visits ? (int) $link->max_visits : null,
+ 'activated_at' => $link->activated_at ? $link->activated_at->toIso8601String() : null,
+ 'expires_at' => $link->expires_at ? $link->expires_at->toIso8601String() : null,
+ 'pixel_meta_id' => $link->pixel_meta_id,
+ 'pixel_google_id' => $link->pixel_google_id,
+ 'pixel_linkedin_id' => $link->pixel_linkedin_id,
+ 'webhook_url' => $link->webhook_url,
+ 'notes' => $link->notes,
+ 'created_at' => $link->created_at->toIso8601String(),
+ ];
+ }
+
+ /**
+ * Dispatch webhook if global or per-link webhook is active for 'created' event.
+ */
+ private function dispatchCreatedWebhook(ShortUrl $shortUrl): void
+ {
+ $targetUrl = $shortUrl->webhook_url;
+ $globalUrl = config('filament-short-url.global_webhook_url');
+ $events = config('filament-short-url.webhook_events', []);
+
+ if (empty($targetUrl) && !empty($globalUrl) && in_array('created', $events)) {
+ $targetUrl = $globalUrl;
+ }
+
+ if (!empty($targetUrl)) {
+ $connection = config('filament-short-url.queue_connection', 'sync');
+ dispatch(new SendWebhookJob(
+ url: $targetUrl,
+ event: 'created',
+ payload: [
+ 'event' => 'created',
+ 'timestamp' => now()->toIso8601String(),
+ 'short_url' => $this->transformLink($shortUrl),
+ ]
+ )->onConnection($connection ?: 'sync'));
+ }
+ }
+}
diff --git a/src/Http/Controllers/ShortUrlRedirectController.php b/src/Http/Controllers/ShortUrlRedirectController.php
index 384b55a..f5ac180 100644
--- a/src/Http/Controllers/ShortUrlRedirectController.php
+++ b/src/Http/Controllers/ShortUrlRedirectController.php
@@ -141,6 +141,15 @@ class ShortUrlRedirectController extends Controller
cache()->forget("filament-short-url:{$shortUrl->url_key}");
}
+ if (! empty($shortUrl->pixel_meta_id) || ! empty($shortUrl->pixel_google_id) || ! empty($shortUrl->pixel_linkedin_id)) {
+ return response(view('filament-short-url::pixel-loading', [
+ 'destination' => $destination,
+ 'pixelMetaId' => $shortUrl->pixel_meta_id,
+ 'pixelGoogleId' => $shortUrl->pixel_google_id,
+ 'pixelLinkedinId' => $shortUrl->pixel_linkedin_id,
+ ]))->header('Content-Type', 'text/html');
+ }
+
return redirect()->away($destination, $shortUrl->redirect_status_code);
}
}
diff --git a/src/Http/Middleware/AuthenticateShortUrlApi.php b/src/Http/Middleware/AuthenticateShortUrlApi.php
new file mode 100644
index 0000000..6a24880
--- /dev/null
+++ b/src/Http/Middleware/AuthenticateShortUrlApi.php
@@ -0,0 +1,50 @@
+header('X-Api-Key');
+
+ if (!$apiKey && $auth = $request->header('Authorization')) {
+ if (str_starts_with(strtolower($auth), 'bearer ')) {
+ $apiKey = substr($auth, 7);
+ }
+ }
+
+ if (empty($apiKey)) {
+ return response()->json([
+ 'error' => 'Unauthorized. API Key is missing.',
+ ], 401);
+ }
+
+ $mgr = app(ShortUrlSettingsManager::class);
+ $keys = $mgr->get('api_keys', []);
+
+ $valid = false;
+ foreach ($keys as $keyObj) {
+ if (($keyObj['key'] ?? '') === $apiKey && (bool) ($keyObj['is_active'] ?? false)) {
+ $valid = true;
+ break;
+ }
+ }
+
+ if (!$valid) {
+ return response()->json([
+ 'error' => 'Unauthorized. Invalid or inactive API Key.',
+ ], 401);
+ }
+
+ return $next($request);
+ }
+}
diff --git a/src/Jobs/SendWebhookJob.php b/src/Jobs/SendWebhookJob.php
new file mode 100644
index 0000000..de7b362
--- /dev/null
+++ b/src/Jobs/SendWebhookJob.php
@@ -0,0 +1,70 @@
+onQueue(config('filament-short-url.queue_name', 'default'));
+ }
+
+ /**
+ * Execute the job.
+ */
+ public function handle(): void
+ {
+ try {
+ $response = Http::timeout(10)
+ ->withHeaders([
+ 'Content-Type' => 'application/json',
+ 'User-Agent' => 'wYachts-ShortUrl-Webhook/1.5',
+ ])
+ ->post($this->url, $this->payload);
+
+ if ($response->failed()) {
+ Log::warning("[FilamentShortUrl] Webhook delivery returned client/server error", [
+ 'url' => $this->url,
+ 'event' => $this->event,
+ 'status' => $response->status(),
+ ]);
+
+ // Throw exception to trigger queue retry
+ throw new \RuntimeException("Webhook failed with status " . $response->status());
+ }
+ } catch (\Throwable $e) {
+ Log::warning("[FilamentShortUrl] Webhook delivery failed", [
+ 'url' => $this->url,
+ 'event' => $this->event,
+ 'error' => $e->getMessage(),
+ ]);
+
+ throw $e;
+ }
+ }
+}
diff --git a/src/Jobs/TrackShortUrlVisitJob.php b/src/Jobs/TrackShortUrlVisitJob.php
index a477a43..bcdf4e8 100644
--- a/src/Jobs/TrackShortUrlVisitJob.php
+++ b/src/Jobs/TrackShortUrlVisitJob.php
@@ -90,6 +90,53 @@ class TrackShortUrlVisitJob implements ShouldQueue
// Fire event for user listeners
ShortUrlVisited::dispatch($shortUrl, $visit);
+ // Trigger Webhook if active
+ $targetUrl = $shortUrl->webhook_url;
+ $globalUrl = config('filament-short-url.global_webhook_url');
+ $events = config('filament-short-url.webhook_events', []);
+
+ if (empty($targetUrl) && ! empty($globalUrl) && in_array('visited', $events)) {
+ $targetUrl = $globalUrl;
+ }
+
+ if (! empty($targetUrl)) {
+ dispatch(new \Bjanczak\FilamentShortUrl\Jobs\SendWebhookJob(
+ url: $targetUrl,
+ event: 'visited',
+ payload: [
+ 'event' => 'visited',
+ 'timestamp' => now()->toIso8601String(),
+ 'short_url' => [
+ 'id' => $shortUrl->id,
+ 'destination_url' => $shortUrl->destination_url,
+ 'url_key' => $shortUrl->url_key,
+ 'short_url' => $shortUrl->getShortUrl(),
+ 'total_visits' => (int) $shortUrl->getRealTimeTotalVisits(),
+ 'unique_visits' => (int) $shortUrl->unique_visits,
+ ],
+ 'visit' => [
+ 'id' => $visit->id,
+ 'visited_at' => $visit->visited_at->toIso8601String(),
+ 'device_type' => $visit->device_type,
+ 'browser' => $visit->browser,
+ 'browser_version' => $visit->browser_version,
+ 'operating_system' => $visit->operating_system,
+ 'operating_system_version' => $visit->operating_system_version,
+ 'country' => $visit->country,
+ 'country_code' => $visit->country_code,
+ 'city' => $visit->city,
+ 'referer_url' => $visit->referer_url,
+ 'referer_host' => $visit->referer_host,
+ 'utm_source' => $visit->utm_source,
+ 'utm_medium' => $visit->utm_medium,
+ 'utm_campaign' => $visit->utm_campaign,
+ 'utm_term' => $visit->utm_term,
+ 'utm_content' => $visit->utm_content,
+ ],
+ ]
+ )->onConnection($this->connection ?: 'sync'));
+ }
+
// Optional GA4 Measurement Protocol integration
if ($shortUrl->ga_tracking_id && config('filament-short-url.ga4.api_secret')) {
$this->sendGa4Hit($shortUrl, $visit);
diff --git a/src/Models/ShortUrl.php b/src/Models/ShortUrl.php
index b81c7f1..0dcc19e 100644
--- a/src/Models/ShortUrl.php
+++ b/src/Models/ShortUrl.php
@@ -80,6 +80,10 @@ class ShortUrl extends Model
'unique_visits',
'max_visits',
'expiration_redirect_url',
+ 'pixel_meta_id',
+ 'pixel_google_id',
+ 'pixel_linkedin_id',
+ 'webhook_url',
];
/** @var array */
@@ -186,6 +190,19 @@ class ShortUrl extends Model
if ($m->activated_at === null && $m->expires_at === null) {
$m->expiration_redirect_url = null;
}
+
+ if (empty($m->pixel_meta_id)) {
+ $m->pixel_meta_id = null;
+ }
+ if (empty($m->pixel_google_id)) {
+ $m->pixel_google_id = null;
+ }
+ if (empty($m->pixel_linkedin_id)) {
+ $m->pixel_linkedin_id = null;
+ }
+ if (empty($m->webhook_url)) {
+ $m->webhook_url = null;
+ }
});
static::saved(function (self $m) {
diff --git a/src/Services/ShortUrlSettingsManager.php b/src/Services/ShortUrlSettingsManager.php
index 6a4e078..f009770 100644
--- a/src/Services/ShortUrlSettingsManager.php
+++ b/src/Services/ShortUrlSettingsManager.php
@@ -76,6 +76,9 @@ class ShortUrlSettingsManager
'qr_gradient_from' => config('filament-short-url.qr_defaults.gradient_from', '#4f46e5'),
'qr_gradient_to' => config('filament-short-url.qr_defaults.gradient_to', '#06b6d4'),
'qr_gradient_type' => config('filament-short-url.qr_defaults.gradient_type', 'linear'),
+ 'global_webhook_url' => null,
+ 'webhook_events' => ['visited'],
+ 'api_keys' => [],
], $stored);
return $this->cache;
@@ -142,6 +145,9 @@ class ShortUrlSettingsManager
'qr_gradient_from',
'qr_gradient_to',
'qr_gradient_type',
+ 'global_webhook_url',
+ 'webhook_events',
+ 'api_keys',
];
$filtered = array_intersect_key($data, array_flip($keys));
@@ -292,6 +298,8 @@ class ShortUrlSettingsManager
'filament-short-url.qr_defaults.gradient_from' => $settings['qr_gradient_from'],
'filament-short-url.qr_defaults.gradient_to' => $settings['qr_gradient_to'],
'filament-short-url.qr_defaults.gradient_type' => $settings['qr_gradient_type'],
+ 'filament-short-url.global_webhook_url' => $settings['global_webhook_url'] ?? null,
+ 'filament-short-url.webhook_events' => $settings['webhook_events'] ?? ['visited'],
]);
}
}