diff --git a/module/CLI/config/cli.config.php b/module/CLI/config/cli.config.php
index 7629d855..012c6800 100644
--- a/module/CLI/config/cli.config.php
+++ b/module/CLI/config/cli.config.php
@@ -13,6 +13,7 @@ return [
             Command\ShortUrl\ListShortUrlsCommand::NAME => Command\ShortUrl\ListShortUrlsCommand::class,
             Command\ShortUrl\GetShortUrlVisitsCommand::NAME => Command\ShortUrl\GetShortUrlVisitsCommand::class,
             Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
+            Command\ShortUrl\DeleteShortUrlVisitsCommand::NAME => Command\ShortUrl\DeleteShortUrlVisitsCommand::class,
 
             Command\Visit\LocateVisitsCommand::NAME => Command\Visit\LocateVisitsCommand::class,
             Command\Visit\DownloadGeoLiteDbCommand::NAME => Command\Visit\DownloadGeoLiteDbCommand::class,
diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php
index e5176f42..384df91d 100644
--- a/module/CLI/config/dependencies.config.php
+++ b/module/CLI/config/dependencies.config.php
@@ -42,6 +42,7 @@ return [
             Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class,
             Command\ShortUrl\GetShortUrlVisitsCommand::class => ConfigAbstractFactory::class,
             Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
+            Command\ShortUrl\DeleteShortUrlVisitsCommand::class => ConfigAbstractFactory::class,
 
             Command\Visit\DownloadGeoLiteDbCommand::class => ConfigAbstractFactory::class,
             Command\Visit\LocateVisitsCommand::class => ConfigAbstractFactory::class,
@@ -88,6 +89,7 @@ return [
         ],
         Command\ShortUrl\GetShortUrlVisitsCommand::class => [Visit\VisitsStatsHelper::class],
         Command\ShortUrl\DeleteShortUrlCommand::class => [ShortUrl\DeleteShortUrlService::class],
+        Command\ShortUrl\DeleteShortUrlVisitsCommand::class => [ShortUrl\ShortUrlVisitsDeleter::class],
 
         Command\Visit\DownloadGeoLiteDbCommand::class => [GeoLite\GeolocationDbUpdater::class],
         Command\Visit\LocateVisitsCommand::class => [
diff --git a/module/CLI/src/Command/Api/DisableKeyCommand.php b/module/CLI/src/Command/Api/DisableKeyCommand.php
index 7296632a..4844121e 100644
--- a/module/CLI/src/Command/Api/DisableKeyCommand.php
+++ b/module/CLI/src/Command/Api/DisableKeyCommand.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 namespace Shlinkio\Shlink\CLI\Command\Api;
 
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
 use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
 use Symfony\Component\Console\Command\Command;
@@ -39,10 +39,10 @@ class DisableKeyCommand extends Command
         try {
             $this->apiKeyService->disable($apiKey);
             $io->success(sprintf('API key "%s" properly disabled', $apiKey));
-            return ExitCodes::EXIT_SUCCESS;
+            return ExitCode::EXIT_SUCCESS;
         } catch (InvalidArgumentException $e) {
             $io->error($e->getMessage());
-            return ExitCodes::EXIT_FAILURE;
+            return ExitCode::EXIT_FAILURE;
         }
     }
 }
diff --git a/module/CLI/src/Command/Api/GenerateKeyCommand.php b/module/CLI/src/Command/Api/GenerateKeyCommand.php
index c89c4fbf..c2d6cf10 100644
--- a/module/CLI/src/Command/Api/GenerateKeyCommand.php
+++ b/module/CLI/src/Command/Api/GenerateKeyCommand.php
@@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Api;
 
 use Cake\Chronos\Chronos;
 use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface;
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\CLI\Util\ShlinkTable;
 use Shlinkio\Shlink\Rest\ApiKey\Role;
 use Shlinkio\Shlink\Rest\Entity\ApiKey;
@@ -109,6 +109,6 @@ class GenerateKeyCommand extends Command
             );
         }
 
-        return ExitCodes::EXIT_SUCCESS;
+        return ExitCode::EXIT_SUCCESS;
     }
 }
diff --git a/module/CLI/src/Command/Api/ListKeysCommand.php b/module/CLI/src/Command/Api/ListKeysCommand.php
index c7e31819..87b239b7 100644
--- a/module/CLI/src/Command/Api/ListKeysCommand.php
+++ b/module/CLI/src/Command/Api/ListKeysCommand.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 namespace Shlinkio\Shlink\CLI\Command\Api;
 
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\CLI\Util\ShlinkTable;
 use Shlinkio\Shlink\Rest\ApiKey\Role;
 use Shlinkio\Shlink\Rest\Entity\ApiKey;
@@ -77,7 +77,7 @@ class ListKeysCommand extends Command
             'Roles',
         ]), $rows);
 
-        return ExitCodes::EXIT_SUCCESS;
+        return ExitCode::EXIT_SUCCESS;
     }
 
     private function determineMessagePattern(ApiKey $apiKey): string
diff --git a/module/CLI/src/Command/Db/CreateDatabaseCommand.php b/module/CLI/src/Command/Db/CreateDatabaseCommand.php
index 95b08da2..f6df9b04 100644
--- a/module/CLI/src/Command/Db/CreateDatabaseCommand.php
+++ b/module/CLI/src/Command/Db/CreateDatabaseCommand.php
@@ -8,7 +8,7 @@ use Doctrine\DBAL\Connection;
 use Doctrine\DBAL\Platforms\SqlitePlatform;
 use Doctrine\ORM\EntityManagerInterface;
 use Doctrine\ORM\Mapping\ClassMetadata;
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
@@ -57,7 +57,7 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
 
         if ($this->schemaExists()) {
             $io->success('Database already exists. Run "db:migrate" command to make sure it is up to date.');
-            return ExitCodes::EXIT_SUCCESS;
+            return ExitCode::EXIT_SUCCESS;
         }
 
         // Create database
@@ -65,7 +65,7 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
         $this->runPhpCommand($output, [self::DOCTRINE_SCRIPT, self::DOCTRINE_CREATE_SCHEMA_COMMAND]);
         $io->success('Database properly created!');
 
-        return ExitCodes::EXIT_SUCCESS;
+        return ExitCode::EXIT_SUCCESS;
     }
 
     private function checkDbExists(): void
diff --git a/module/CLI/src/Command/Db/MigrateDatabaseCommand.php b/module/CLI/src/Command/Db/MigrateDatabaseCommand.php
index 379e57e0..a912cf24 100644
--- a/module/CLI/src/Command/Db/MigrateDatabaseCommand.php
+++ b/module/CLI/src/Command/Db/MigrateDatabaseCommand.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 namespace Shlinkio\Shlink\CLI\Command\Db;
 
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Style\SymfonyStyle;
@@ -31,6 +31,6 @@ class MigrateDatabaseCommand extends AbstractDatabaseCommand
         $this->runPhpCommand($output, [self::DOCTRINE_MIGRATIONS_SCRIPT, self::DOCTRINE_MIGRATE_COMMAND]);
         $io->success('Database properly migrated!');
 
-        return ExitCodes::EXIT_SUCCESS;
+        return ExitCode::EXIT_SUCCESS;
     }
 }
diff --git a/module/CLI/src/Command/Domain/DomainRedirectsCommand.php b/module/CLI/src/Command/Domain/DomainRedirectsCommand.php
index c546fd5b..4a3f8062 100644
--- a/module/CLI/src/Command/Domain/DomainRedirectsCommand.php
+++ b/module/CLI/src/Command/Domain/DomainRedirectsCommand.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 namespace Shlinkio\Shlink\CLI\Command\Domain;
 
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\Core\Config\NotFoundRedirects;
 use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
 use Shlinkio\Shlink\Core\Domain\Model\DomainItem;
@@ -109,6 +109,6 @@ class DomainRedirectsCommand extends Command
 
         $io->success(sprintf('"Not found" redirects properly set for "%s"', $domainAuthority));
 
-        return ExitCodes::EXIT_SUCCESS;
+        return ExitCode::EXIT_SUCCESS;
     }
 }
diff --git a/module/CLI/src/Command/Domain/ListDomainsCommand.php b/module/CLI/src/Command/Domain/ListDomainsCommand.php
index 8f2ee22c..11a0f5b9 100644
--- a/module/CLI/src/Command/Domain/ListDomainsCommand.php
+++ b/module/CLI/src/Command/Domain/ListDomainsCommand.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 namespace Shlinkio\Shlink\CLI\Command\Domain;
 
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\CLI\Util\ShlinkTable;
 use Shlinkio\Shlink\Core\Config\NotFoundRedirectConfigInterface;
 use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
@@ -59,7 +59,7 @@ class ListDomainsCommand extends Command
             }),
         );
 
-        return ExitCodes::EXIT_SUCCESS;
+        return ExitCode::EXIT_SUCCESS;
     }
 
     private function notFoundRedirectsToString(NotFoundRedirectConfigInterface $config): string
diff --git a/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php b/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php
index a998a677..f55f247d 100644
--- a/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php
+++ b/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
 
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
 use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
 use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
@@ -141,7 +141,7 @@ class CreateShortUrlCommand extends Command
         $longUrl = $input->getArgument('longUrl');
         if (empty($longUrl)) {
             $io->error('A URL was not provided!');
-            return ExitCodes::EXIT_FAILURE;
+            return ExitCode::EXIT_FAILURE;
         }
 
         $explodeWithComma = curry(explode(...))(',');
@@ -176,10 +176,10 @@ class CreateShortUrlCommand extends Command
                 sprintf('Processed long URL: <info>%s</info>', $longUrl),
                 sprintf('Generated short URL: <info>%s</info>', $this->stringifier->stringify($result->shortUrl)),
             ]);
-            return ExitCodes::EXIT_SUCCESS;
+            return ExitCode::EXIT_SUCCESS;
         } catch (InvalidUrlException | NonUniqueSlugException $e) {
             $io->error($e->getMessage());
-            return ExitCodes::EXIT_FAILURE;
+            return ExitCode::EXIT_FAILURE;
         }
     }
 
diff --git a/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php b/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php
index 1db5b1f6..11cfa270 100644
--- a/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php
+++ b/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
 
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\Core\Exception;
 use Shlinkio\Shlink\Core\ShortUrl\DeleteShortUrlServiceInterface;
 use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
@@ -55,10 +55,10 @@ class DeleteShortUrlCommand extends Command
 
         try {
             $this->runDelete($io, $identifier, $ignoreThreshold);
-            return ExitCodes::EXIT_SUCCESS;
+            return ExitCode::EXIT_SUCCESS;
         } catch (Exception\ShortUrlNotFoundException $e) {
             $io->error($e->getMessage());
-            return ExitCodes::EXIT_FAILURE;
+            return ExitCode::EXIT_FAILURE;
         } catch (Exception\DeleteShortUrlException $e) {
             return $this->retry($io, $identifier, $e->getMessage());
         }
@@ -75,7 +75,7 @@ class DeleteShortUrlCommand extends Command
             $io->warning('Short URL was not deleted.');
         }
 
-        return $forceDelete ? ExitCodes::EXIT_SUCCESS : ExitCodes::EXIT_WARNING;
+        return $forceDelete ? ExitCode::EXIT_SUCCESS : ExitCode::EXIT_WARNING;
     }
 
     private function runDelete(SymfonyStyle $io, ShortUrlIdentifier $identifier, bool $ignoreThreshold): void
diff --git a/module/CLI/src/Command/ShortUrl/DeleteShortUrlVisitsCommand.php b/module/CLI/src/Command/ShortUrl/DeleteShortUrlVisitsCommand.php
new file mode 100644
index 00000000..fa7a8ee6
--- /dev/null
+++ b/module/CLI/src/Command/ShortUrl/DeleteShortUrlVisitsCommand.php
@@ -0,0 +1,72 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
+
+use Shlinkio\Shlink\CLI\Util\ExitCode;
+use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
+use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
+use Shlinkio\Shlink\Core\ShortUrl\ShortUrlVisitsDeleterInterface;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+use function sprintf;
+
+class DeleteShortUrlVisitsCommand extends Command
+{
+    public const NAME = 'short-url:delete-visits';
+
+    public function __construct(private readonly ShortUrlVisitsDeleterInterface $deleter)
+    {
+        parent::__construct();
+    }
+
+    protected function configure(): void
+    {
+        $this
+            ->setName(self::NAME)
+            ->setDescription('Deletes visits from a short URL')
+            ->addArgument(
+                'shortCode',
+                InputArgument::REQUIRED,
+                'The short code for the short URL which visits will be deleted',
+            )
+            ->addOption(
+                'domain',
+                'd',
+                InputOption::VALUE_REQUIRED,
+                'The domain if the short code does not belong to the default one',
+            );
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output): ?int
+    {
+        $identifier = ShortUrlIdentifier::fromCli($input);
+        $io = new SymfonyStyle($input, $output);
+        if (! $this->confirm($io)) {
+            $io->info('Operation aborted');
+            return ExitCode::EXIT_SUCCESS;
+        }
+
+        try {
+            $result = $this->deleter->deleteShortUrlVisits($identifier);
+            $io->success(sprintf('Successfully deleted %s visits', $result->affectedItems));
+
+            return ExitCode::EXIT_SUCCESS;
+        } catch (ShortUrlNotFoundException) {
+            $io->warning(sprintf('Short URL not found for "%s"', $identifier->__toString()));
+            return ExitCode::EXIT_WARNING;
+        }
+    }
+
+    private function confirm(SymfonyStyle $io): bool
+    {
+        $io->warning('You are about to delete all visits for a short URL. This operation cannot be undone.');
+        return $io->confirm('<comment>Continue deleting visits?</comment>', false);
+    }
+}
diff --git a/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php b/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php
index f31de184..14ea1851 100644
--- a/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php
+++ b/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php
@@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
 
 use Shlinkio\Shlink\CLI\Input\EndDateOption;
 use Shlinkio\Shlink\CLI\Input\StartDateOption;
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\CLI\Util\ShlinkTable;
 use Shlinkio\Shlink\Common\Paginator\Paginator;
 use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait;
@@ -173,7 +173,7 @@ class ListShortUrlsCommand extends Command
         $io->newLine();
         $io->success('Short URLs properly listed');
 
-        return ExitCodes::EXIT_SUCCESS;
+        return ExitCode::EXIT_SUCCESS;
     }
 
     private function renderPage(
diff --git a/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php b/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php
index 8d54edd2..aec0a843 100644
--- a/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php
+++ b/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
 
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
 use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
 use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
@@ -56,10 +56,10 @@ class ResolveUrlCommand extends Command
         try {
             $url = $this->urlResolver->resolveShortUrl(ShortUrlIdentifier::fromCli($input));
             $output->writeln(sprintf('Long URL: <info>%s</info>', $url->getLongUrl()));
-            return ExitCodes::EXIT_SUCCESS;
+            return ExitCode::EXIT_SUCCESS;
         } catch (ShortUrlNotFoundException $e) {
             $io->error($e->getMessage());
-            return ExitCodes::EXIT_FAILURE;
+            return ExitCode::EXIT_FAILURE;
         }
     }
 }
diff --git a/module/CLI/src/Command/Tag/DeleteTagsCommand.php b/module/CLI/src/Command/Tag/DeleteTagsCommand.php
index 5a4f81ac..151c5892 100644
--- a/module/CLI/src/Command/Tag/DeleteTagsCommand.php
+++ b/module/CLI/src/Command/Tag/DeleteTagsCommand.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 namespace Shlinkio\Shlink\CLI\Command\Tag;
 
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Input\InputInterface;
@@ -41,11 +41,11 @@ class DeleteTagsCommand extends Command
 
         if (empty($tagNames)) {
             $io->warning('You have to provide at least one tag name');
-            return ExitCodes::EXIT_WARNING;
+            return ExitCode::EXIT_WARNING;
         }
 
         $this->tagService->deleteTags($tagNames);
         $io->success('Tags properly deleted');
-        return ExitCodes::EXIT_SUCCESS;
+        return ExitCode::EXIT_SUCCESS;
     }
 }
diff --git a/module/CLI/src/Command/Tag/ListTagsCommand.php b/module/CLI/src/Command/Tag/ListTagsCommand.php
index 02116d79..41ca9b60 100644
--- a/module/CLI/src/Command/Tag/ListTagsCommand.php
+++ b/module/CLI/src/Command/Tag/ListTagsCommand.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 namespace Shlinkio\Shlink\CLI\Command\Tag;
 
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\CLI\Util\ShlinkTable;
 use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
 use Shlinkio\Shlink\Core\Tag\Model\TagsParams;
@@ -34,7 +34,7 @@ class ListTagsCommand extends Command
     protected function execute(InputInterface $input, OutputInterface $output): ?int
     {
         ShlinkTable::default($output)->render(['Name', 'URLs amount', 'Visits amount'], $this->getTagsRows());
-        return ExitCodes::EXIT_SUCCESS;
+        return ExitCode::EXIT_SUCCESS;
     }
 
     private function getTagsRows(): array
diff --git a/module/CLI/src/Command/Tag/RenameTagCommand.php b/module/CLI/src/Command/Tag/RenameTagCommand.php
index 85377a18..1da3b983 100644
--- a/module/CLI/src/Command/Tag/RenameTagCommand.php
+++ b/module/CLI/src/Command/Tag/RenameTagCommand.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 namespace Shlinkio\Shlink\CLI\Command\Tag;
 
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\Core\Exception\TagConflictException;
 use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
 use Shlinkio\Shlink\Core\Tag\Model\TagRenaming;
@@ -42,10 +42,10 @@ class RenameTagCommand extends Command
         try {
             $this->tagService->renameTag(TagRenaming::fromNames($oldName, $newName));
             $io->success('Tag properly renamed.');
-            return ExitCodes::EXIT_SUCCESS;
+            return ExitCode::EXIT_SUCCESS;
         } catch (TagNotFoundException | TagConflictException $e) {
             $io->error($e->getMessage());
-            return ExitCodes::EXIT_FAILURE;
+            return ExitCode::EXIT_FAILURE;
         }
     }
 }
diff --git a/module/CLI/src/Command/Util/AbstractLockedCommand.php b/module/CLI/src/Command/Util/AbstractLockedCommand.php
index d1e45fd8..ae930496 100644
--- a/module/CLI/src/Command/Util/AbstractLockedCommand.php
+++ b/module/CLI/src/Command/Util/AbstractLockedCommand.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 namespace Shlinkio\Shlink\CLI\Command\Util;
 
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
@@ -28,7 +28,7 @@ abstract class AbstractLockedCommand extends Command
             $output->writeln(
                 sprintf('<comment>Command "%s" is already in progress. Skipping.</comment>', $lockConfig->lockName),
             );
-            return ExitCodes::EXIT_WARNING;
+            return ExitCode::EXIT_WARNING;
         }
 
         try {
diff --git a/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php
index 2cd9c0c8..ba518656 100644
--- a/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php
+++ b/module/CLI/src/Command/Visit/AbstractVisitsListCommand.php
@@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
 
 use Shlinkio\Shlink\CLI\Input\EndDateOption;
 use Shlinkio\Shlink\CLI\Input\StartDateOption;
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\CLI\Util\ShlinkTable;
 use Shlinkio\Shlink\Common\Paginator\Paginator;
 use Shlinkio\Shlink\Common\Util\DateRange;
@@ -43,7 +43,7 @@ abstract class AbstractVisitsListCommand extends Command
 
         ShlinkTable::default($output)->render($headers, $rows);
 
-        return ExitCodes::EXIT_SUCCESS;
+        return ExitCode::EXIT_SUCCESS;
     }
 
     private function resolveRowsAndHeaders(Paginator $paginator): array
diff --git a/module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php b/module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php
index c4384d33..23600530 100644
--- a/module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php
+++ b/module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php
@@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
 
 use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
 use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface;
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Helper\ProgressBar;
 use Symfony\Component\Console\Input\InputInterface;
@@ -56,7 +56,7 @@ class DownloadGeoLiteDbCommand extends Command
                 $io->success('GeoLite2 db file properly downloaded.');
             }
 
-            return ExitCodes::EXIT_SUCCESS;
+            return ExitCode::EXIT_SUCCESS;
         } catch (GeolocationDbUpdateFailedException $e) {
             $olderDbExists = $e->olderDbExists();
 
@@ -72,7 +72,7 @@ class DownloadGeoLiteDbCommand extends Command
                 $this->getApplication()?->renderThrowable($e, $io);
             }
 
-            return $olderDbExists ? ExitCodes::EXIT_WARNING : ExitCodes::EXIT_FAILURE;
+            return $olderDbExists ? ExitCode::EXIT_WARNING : ExitCode::EXIT_FAILURE;
         }
     }
 }
diff --git a/module/CLI/src/Command/Visit/LocateVisitsCommand.php b/module/CLI/src/Command/Visit/LocateVisitsCommand.php
index d83c91e0..09e53556 100644
--- a/module/CLI/src/Command/Visit/LocateVisitsCommand.php
+++ b/module/CLI/src/Command/Visit/LocateVisitsCommand.php
@@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
 
 use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand;
 use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\Common\Util\IpAddress;
 use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
 use Shlinkio\Shlink\Core\Visit\Entity\Visit;
@@ -116,14 +116,14 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
             }
 
             $this->io->success('Finished locating visits');
-            return ExitCodes::EXIT_SUCCESS;
+            return ExitCode::EXIT_SUCCESS;
         } catch (Throwable $e) {
             $this->io->error($e->getMessage());
             if ($this->io->isVerbose()) {
                 $this->getApplication()?->renderThrowable($e, $this->io);
             }
 
-            return ExitCodes::EXIT_FAILURE;
+            return ExitCode::EXIT_FAILURE;
         }
     }
 
@@ -171,7 +171,7 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
         $downloadDbCommand = $cliApp->find(DownloadGeoLiteDbCommand::NAME);
         $exitCode = $downloadDbCommand->run(new ArrayInput([]), $this->io);
 
-        if ($exitCode === ExitCodes::EXIT_FAILURE) {
+        if ($exitCode === ExitCode::EXIT_FAILURE) {
             throw new RuntimeException('It is not possible to locate visits without a GeoLite2 db file.');
         }
     }
diff --git a/module/CLI/src/Util/ExitCodes.php b/module/CLI/src/Util/ExitCode.php
similarity index 89%
rename from module/CLI/src/Util/ExitCodes.php
rename to module/CLI/src/Util/ExitCode.php
index d915796a..128b9f52 100644
--- a/module/CLI/src/Util/ExitCodes.php
+++ b/module/CLI/src/Util/ExitCode.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 namespace Shlinkio\Shlink\CLI\Util;
 
-final class ExitCodes
+final class ExitCode
 {
     public const EXIT_SUCCESS = 0;
     public const EXIT_FAILURE = -1;
diff --git a/module/CLI/test-cli/Command/CreateShortUrlTest.php b/module/CLI/test-cli/Command/CreateShortUrlTest.php
index d4d8a583..c2e96611 100644
--- a/module/CLI/test-cli/Command/CreateShortUrlTest.php
+++ b/module/CLI/test-cli/Command/CreateShortUrlTest.php
@@ -7,7 +7,7 @@ namespace ShlinkioCliTest\Shlink\CLI\Command;
 use PHPUnit\Framework\Attributes\Test;
 use Shlinkio\Shlink\CLI\Command\ShortUrl\CreateShortUrlCommand;
 use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
 
 class CreateShortUrlTest extends CliTestCase
@@ -22,7 +22,7 @@ class CreateShortUrlTest extends CliTestCase
             [CreateShortUrlCommand::NAME, 'https://example.com', '--domain', $defaultDomain, '--custom-slug', $slug],
         );
 
-        self::assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode);
+        self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
         self::assertStringContainsString('Generated short URL: http://' . $defaultDomain . '/' . $slug, $output);
 
         [$listOutput] = $this->exec([ListShortUrlsCommand::NAME, '--show-domain', '--search-term', $slug]);
diff --git a/module/CLI/test-cli/Command/GenerateApiKeyTest.php b/module/CLI/test-cli/Command/GenerateApiKeyTest.php
index c98dc237..7d90c336 100644
--- a/module/CLI/test-cli/Command/GenerateApiKeyTest.php
+++ b/module/CLI/test-cli/Command/GenerateApiKeyTest.php
@@ -6,7 +6,7 @@ namespace ShlinkioCliTest\Shlink\CLI\Command;
 
 use PHPUnit\Framework\Attributes\Test;
 use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
 
 class GenerateApiKeyTest extends CliTestCase
@@ -17,6 +17,6 @@ class GenerateApiKeyTest extends CliTestCase
         [$output, $exitCode] = $this->exec([GenerateKeyCommand::NAME]);
 
         self::assertStringContainsString('[OK] Generated API key', $output);
-        self::assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode);
+        self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
     }
 }
diff --git a/module/CLI/test-cli/Command/ListApiKeysTest.php b/module/CLI/test-cli/Command/ListApiKeysTest.php
index 80a1134d..f8781d54 100644
--- a/module/CLI/test-cli/Command/ListApiKeysTest.php
+++ b/module/CLI/test-cli/Command/ListApiKeysTest.php
@@ -8,7 +8,7 @@ use Cake\Chronos\Chronos;
 use PHPUnit\Framework\Attributes\DataProvider;
 use PHPUnit\Framework\Attributes\Test;
 use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
 
 class ListApiKeysTest extends CliTestCase
@@ -19,7 +19,7 @@ class ListApiKeysTest extends CliTestCase
         [$output, $exitCode] = $this->exec([ListKeysCommand::NAME, ...$flags]);
 
         self::assertEquals($expectedOutput, $output);
-        self::assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode);
+        self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
     }
 
     public static function provideFlags(): iterable
diff --git a/module/CLI/test/Command/Domain/ListDomainsCommandTest.php b/module/CLI/test/Command/Domain/ListDomainsCommandTest.php
index ad31d86d..05cc95eb 100644
--- a/module/CLI/test/Command/Domain/ListDomainsCommandTest.php
+++ b/module/CLI/test/Command/Domain/ListDomainsCommandTest.php
@@ -9,7 +9,7 @@ use PHPUnit\Framework\Attributes\Test;
 use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 use Shlinkio\Shlink\CLI\Command\Domain\ListDomainsCommand;
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\Core\Config\NotFoundRedirects;
 use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
 use Shlinkio\Shlink\Core\Domain\Entity\Domain;
@@ -53,7 +53,7 @@ class ListDomainsCommandTest extends TestCase
         $this->commandTester->execute($input);
 
         self::assertEquals($expectedOutput, $this->commandTester->getDisplay());
-        self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
+        self::assertEquals(ExitCode::EXIT_SUCCESS, $this->commandTester->getStatusCode());
     }
 
     public static function provideInputsAndOutputs(): iterable
diff --git a/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php
index 60482138..46063485 100644
--- a/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php
+++ b/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php
@@ -11,7 +11,7 @@ use PHPUnit\Framework\Attributes\Test;
 use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 use Shlinkio\Shlink\CLI\Command\ShortUrl\CreateShortUrlCommand;
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
 use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
 use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
@@ -65,7 +65,7 @@ class CreateShortUrlCommandTest extends TestCase
         ], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
         $output = $this->commandTester->getDisplay();
 
-        self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
+        self::assertEquals(ExitCode::EXIT_SUCCESS, $this->commandTester->getStatusCode());
         self::assertStringContainsString('stringified_short_url', $output);
         self::assertStringNotContainsString('but the real-time updates cannot', $output);
     }
@@ -82,7 +82,7 @@ class CreateShortUrlCommandTest extends TestCase
         $this->commandTester->execute(['longUrl' => $url]);
         $output = $this->commandTester->getDisplay();
 
-        self::assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
+        self::assertEquals(ExitCode::EXIT_FAILURE, $this->commandTester->getStatusCode());
         self::assertStringContainsString('Provided URL http://domain.com/invalid is invalid.', $output);
     }
 
@@ -97,7 +97,7 @@ class CreateShortUrlCommandTest extends TestCase
         $this->commandTester->execute(['longUrl' => 'http://domain.com/invalid', '--custom-slug' => 'my-slug']);
         $output = $this->commandTester->getDisplay();
 
-        self::assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
+        self::assertEquals(ExitCode::EXIT_FAILURE, $this->commandTester->getStatusCode());
         self::assertStringContainsString('Provided slug "my-slug" is already in use', $output);
     }
 
@@ -121,7 +121,7 @@ class CreateShortUrlCommandTest extends TestCase
         ]);
         $output = $this->commandTester->getDisplay();
 
-        self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
+        self::assertEquals(ExitCode::EXIT_SUCCESS, $this->commandTester->getStatusCode());
         self::assertStringContainsString('stringified_short_url', $output);
     }
 
@@ -139,7 +139,7 @@ class CreateShortUrlCommandTest extends TestCase
         $input['longUrl'] = 'http://domain.com/foo/bar';
         $this->commandTester->execute($input);
 
-        self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
+        self::assertEquals(ExitCode::EXIT_SUCCESS, $this->commandTester->getStatusCode());
     }
 
     public static function provideDomains(): iterable
diff --git a/module/CLI/test/Command/Visit/DownloadGeoLiteDbCommandTest.php b/module/CLI/test/Command/Visit/DownloadGeoLiteDbCommandTest.php
index 7f2cb3ac..7e904caa 100644
--- a/module/CLI/test/Command/Visit/DownloadGeoLiteDbCommandTest.php
+++ b/module/CLI/test/Command/Visit/DownloadGeoLiteDbCommandTest.php
@@ -12,7 +12,7 @@ use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
 use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
 use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface;
 use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult;
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
 use Symfony\Component\Console\Tester\CommandTester;
 
@@ -65,12 +65,12 @@ class DownloadGeoLiteDbCommandTest extends TestCase
         yield 'existing db' => [
             true,
             '[WARNING] GeoLite2 db file update failed. Visits will continue to be located',
-            ExitCodes::EXIT_WARNING,
+            ExitCode::EXIT_WARNING,
         ];
         yield 'not existing db' => [
             false,
             '[ERROR] GeoLite2 db file download failed. It will not be possible to locate',
-            ExitCodes::EXIT_FAILURE,
+            ExitCode::EXIT_FAILURE,
         ];
     }
 
@@ -86,7 +86,7 @@ class DownloadGeoLiteDbCommandTest extends TestCase
         $exitCode = $this->commandTester->getStatusCode();
 
         self::assertStringContainsString($expectedMessage, $output);
-        self::assertSame(ExitCodes::EXIT_SUCCESS, $exitCode);
+        self::assertSame(ExitCode::EXIT_SUCCESS, $exitCode);
     }
 
     public static function provideSuccessParams(): iterable
diff --git a/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php b/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php
index aa775a24..6ff8c242 100644
--- a/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php
+++ b/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php
@@ -10,7 +10,7 @@ use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
 use Shlinkio\Shlink\CLI\Command\Visit\LocateVisitsCommand;
-use Shlinkio\Shlink\CLI\Util\ExitCodes;
+use Shlinkio\Shlink\CLI\Util\ExitCode;
 use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
 use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
 use Shlinkio\Shlink\Core\Visit\Entity\Visit;
@@ -85,7 +85,7 @@ class LocateVisitsCommandTest extends TestCase
         $this->visitToLocation->expects(
             $this->exactly($expectedUnlocatedCalls + $expectedEmptyCalls + $expectedAllCalls),
         )->method('resolveVisitLocation')->withAnyParameters()->willReturn(Location::emptyInstance());
-        $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
+        $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_SUCCESS);
 
         $this->commandTester->setInputs(['y']);
         $this->commandTester->execute($args);
@@ -118,7 +118,7 @@ class LocateVisitsCommandTest extends TestCase
                            ->withAnyParameters()
                            ->willReturnCallback($this->invokeHelperMethods($visit, $location));
         $this->visitToLocation->expects($this->once())->method('resolveVisitLocation')->willThrowException($e);
-        $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
+        $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_SUCCESS);
 
         $this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
 
@@ -147,7 +147,7 @@ class LocateVisitsCommandTest extends TestCase
         $this->visitToLocation->expects($this->once())->method('resolveVisitLocation')->willThrowException(
             IpCannotBeLocatedException::forError(WrongIpException::fromIpAddress('1.2.3.4')),
         );
-        $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
+        $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_SUCCESS);
 
         $this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
 
@@ -171,7 +171,7 @@ class LocateVisitsCommandTest extends TestCase
 
         $this->visitService->expects($this->never())->method('locateUnlocatedVisits');
         $this->visitToLocation->expects($this->never())->method('resolveVisitLocation');
-        $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
+        $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_SUCCESS);
 
         $this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
         $output = $this->commandTester->getDisplay();
@@ -186,7 +186,7 @@ class LocateVisitsCommandTest extends TestCase
     public function showsProperMessageWhenGeoLiteUpdateFails(): void
     {
         $this->lock->method('acquire')->with($this->isFalse())->willReturn(true);
-        $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_FAILURE);
+        $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_FAILURE);
         $this->visitService->expects($this->never())->method('locateUnlocatedVisits');
 
         $this->commandTester->execute([]);
@@ -199,7 +199,7 @@ class LocateVisitsCommandTest extends TestCase
     public function providingAllFlagOnItsOwnDisplaysNotice(): void
     {
         $this->lock->method('acquire')->with($this->isFalse())->willReturn(true);
-        $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
+        $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_SUCCESS);
 
         $this->commandTester->execute(['--all' => true]);
         $output = $this->commandTester->getDisplay();
@@ -210,7 +210,7 @@ class LocateVisitsCommandTest extends TestCase
     #[Test, DataProvider('provideAbortInputs')]
     public function processingAllCancelsCommandIfUserDoesNotActivelyAgreeToConfirmation(array $inputs): void
     {
-        $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
+        $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_SUCCESS);
 
         $this->expectException(RuntimeException::class);
         $this->expectExceptionMessage('Execution aborted');
diff --git a/module/Core/src/ShortUrl/Model/ShortUrlIdentifier.php b/module/Core/src/ShortUrl/Model/ShortUrlIdentifier.php
index bb3b4af6..78becbed 100644
--- a/module/Core/src/ShortUrl/Model/ShortUrlIdentifier.php
+++ b/module/Core/src/ShortUrl/Model/ShortUrlIdentifier.php
@@ -8,6 +8,8 @@ use Psr\Http\Message\ServerRequestInterface;
 use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
 use Symfony\Component\Console\Input\InputInterface;
 
+use function sprintf;
+
 final class ShortUrlIdentifier
 {
     private function __construct(public readonly string $shortCode, public readonly ?string $domain = null)
@@ -54,4 +56,13 @@ final class ShortUrlIdentifier
     {
         return new self($shortCode, $domain);
     }
+
+    public function __toString(): string
+    {
+        if ($this->domain === null) {
+            return $this->shortCode;
+        }
+
+        return sprintf('%s/%s', $this->domain, $this->shortCode);
+    }
 }
diff --git a/module/Core/src/ShortUrl/ShortUrlVisitsDeleter.php b/module/Core/src/ShortUrl/ShortUrlVisitsDeleter.php
index c202c5c2..8ad6713f 100644
--- a/module/Core/src/ShortUrl/ShortUrlVisitsDeleter.php
+++ b/module/Core/src/ShortUrl/ShortUrlVisitsDeleter.php
@@ -21,7 +21,7 @@ class ShortUrlVisitsDeleter implements ShortUrlVisitsDeleterInterface
     /**
      * @throws ShortUrlNotFoundException
      */
-    public function deleteShortUrlVisits(ShortUrlIdentifier $identifier, ?ApiKey $apiKey): BulkDeleteResult
+    public function deleteShortUrlVisits(ShortUrlIdentifier $identifier, ?ApiKey $apiKey = null): BulkDeleteResult
     {
         $shortUrl = $this->resolver->resolveShortUrl($identifier, $apiKey);
         return new BulkDeleteResult($this->repository->deleteShortUrlVisits($shortUrl));
diff --git a/module/Core/src/ShortUrl/ShortUrlVisitsDeleterInterface.php b/module/Core/src/ShortUrl/ShortUrlVisitsDeleterInterface.php
index b0ac0e6a..46e9fde5 100644
--- a/module/Core/src/ShortUrl/ShortUrlVisitsDeleterInterface.php
+++ b/module/Core/src/ShortUrl/ShortUrlVisitsDeleterInterface.php
@@ -14,5 +14,5 @@ interface ShortUrlVisitsDeleterInterface
     /**
      * @throws ShortUrlNotFoundException
      */
-    public function deleteShortUrlVisits(ShortUrlIdentifier $identifier, ?ApiKey $apiKey): BulkDeleteResult;
+    public function deleteShortUrlVisits(ShortUrlIdentifier $identifier, ?ApiKey $apiKey = null): BulkDeleteResult;
 }
diff --git a/module/Core/test-db/Visit/Repository/VisitDeleterRepositoryTest.php b/module/Core/test-db/Visit/Repository/VisitDeleterRepositoryTest.php
index 1598fc94..53b1585f 100644
--- a/module/Core/test-db/Visit/Repository/VisitDeleterRepositoryTest.php
+++ b/module/Core/test-db/Visit/Repository/VisitDeleterRepositoryTest.php
@@ -52,7 +52,6 @@ class VisitDeleterRepositoryTest extends DatabaseTestCase
 
         $this->getEntityManager()->flush();
 
-        self::assertEquals(0, $this->repo->deleteShortUrlVisits(ShortUrl::withLongUrl('https://invalid')->setId('99')));
         self::assertEquals(2, $this->repo->deleteShortUrlVisits($shortUrl1));
         self::assertEquals(0, $this->repo->deleteShortUrlVisits($shortUrl1));
         self::assertEquals(4, $this->repo->deleteShortUrlVisits($shortUrl2));
diff --git a/module/Rest/test-api/Action/CreateShortUrlTest.php b/module/Rest/test-api/Action/CreateShortUrlTest.php
index 54d1d45a..dfdd170e 100644
--- a/module/Rest/test-api/Action/CreateShortUrlTest.php
+++ b/module/Rest/test-api/Action/CreateShortUrlTest.php
@@ -320,27 +320,6 @@ class CreateShortUrlTest extends ApiTestCase
         yield 'example domain' => ['example.com'];
     }
 
-    #[Test, DataProvider('provideTwitterUrls')]
-    public function urlsWithBotProtectionCanBeShortenedWithUrlValidationEnabled(string $longUrl): void
-    {
-        // Requests to Twitter are randomly failing from GitHub actions. Let's skip this test there.
-        // This is a deprecated and low-used feature anyway.
-        if (env('CI', false)) {
-            $this->markTestSkipped();
-        }
-
-        [$statusCode] = $this->createShortUrl(['longUrl' => $longUrl, 'validateUrl' => true]);
-        self::assertEquals(self::STATUS_OK, $statusCode);
-    }
-
-    public static function provideTwitterUrls(): iterable
-    {
-        yield ['https://twitter.com/shlinkio'];
-        yield ['https://mobile.twitter.com/shlinkio'];
-        yield ['https://twitter.com/shlinkio/status/1360637738421268481'];
-        yield ['https://mobile.twitter.com/shlinkio/status/1360637738421268481'];
-    }
-
     #[Test]
     public function canCreateShortUrlsWithEmojis(): void
     {