diff --git a/src/HOTP.php b/src/HOTP.php
index dd297ace34e6906d27a791a0e76772a6b7916224..8dc18d3ef5421bb55af7f6c66e6988401220ffad 100644
--- a/src/HOTP.php
+++ b/src/HOTP.php
@@ -12,33 +12,36 @@ use function is_int;
  */
 final class HOTP extends OTP implements HOTPInterface
 {
-    protected function __construct(null|string $secret, int $counter, string $digest, int $digits)
-    {
-        parent::__construct($secret, $digest, $digits);
-        $this->setCounter($counter);
-    }
-
     public static function create(
         null|string $secret = null,
-        int $counter = 0,
-        string $digest = 'sha1',
-        int $digits = 6
+        int $counter = self::DEFAULT_COUNTER,
+        string $digest = self::DEFAULT_DIGEST,
+        int $digits = self::DEFAULT_DIGITS
     ): self {
-        return new self($secret, $counter, $digest, $digits);
+        $htop = $secret !== null
+            ? self::createFromSecret($secret)
+            : self::generate()
+        ;
+        $htop->setCounter($counter);
+        $htop->setDigest($digest);
+        $htop->setDigits($digits);
+
+        return $htop;
     }
 
-    public static function createFromSecret(
-        string $secret,
-        int $counter = 0,
-        string $digest = 'sha1',
-        int $digits = 6
-    ): self {
-        return new self($secret, $counter, $digest, $digits);
+    public static function createFromSecret(string $secret): self
+    {
+        $htop = new self($secret);
+        $htop->setCounter(self::DEFAULT_COUNTER);
+        $htop->setDigest(self::DEFAULT_DIGEST);
+        $htop->setDigits(self::DEFAULT_DIGITS);
+
+        return $htop;
     }
 
-    public static function generate(int $counter = 0, string $digest = 'sha1', int $digits = 6): self
+    public static function generate(): self
     {
-        return new self(self::generateSecret(), $counter, $digest, $digits);
+        return self::createFromSecret(self::generateSecret());
     }
 
     public function getCounter(): int
@@ -72,7 +75,7 @@ final class HOTP extends OTP implements HOTPInterface
         return $this->verifyOtpWithWindow($otp, $counter, $window);
     }
 
-    protected function setCounter(int $counter): void
+    public function setCounter(int $counter): void
     {
         $this->setParameter('counter', $counter);
     }
diff --git a/src/HOTPInterface.php b/src/HOTPInterface.php
index aa1f004dc41d036cf831ab86b083117241d2da04..853e76c0757d516eaa0c806861de3aa0f863b40e 100644
--- a/src/HOTPInterface.php
+++ b/src/HOTPInterface.php
@@ -6,6 +6,8 @@ namespace OTPHP;
 
 interface HOTPInterface extends OTPInterface
 {
+    public const DEFAULT_COUNTER = 0;
+
     /**
      * The initial counter (a positive integer).
      */
@@ -16,6 +18,9 @@ interface HOTPInterface extends OTPInterface
      *
      * If the secret is null, a random 64 bytes secret will be generated.
      *
+     * @param null|non-empty-string $secret
+     * @param non-empty-string $digest
+     *
      * @deprecated Deprecated since v11.1, use ::createFromSecret or ::generate instead
      */
     public static function create(
@@ -25,20 +30,5 @@ interface HOTPInterface extends OTPInterface
         int $digits = 6
     ): self;
 
-    /**
-     * Create a TOTP object from an existing secret.
-     *
-     * @param non-empty-string $secret
-     */
-    public static function createFromSecret(
-        string $secret,
-        int $counter = 0,
-        string $digest = 'sha1',
-        int $digits = 6
-    ): self;
-
-    /**
-     * Create a new HOTP object. A random 64 bytes secret will be generated.
-     */
-    public static function generate(int $counter = 0, string $digest = 'sha1', int $digits = 6): self;
+    public function setCounter(int $counter): void;
 }
diff --git a/src/OTP.php b/src/OTP.php
index d51ea7b3cfa21e86ce4eab9b64d650f0bf0e0c42..ba640a3d2ea3e66b7f5b698f2a871d2d500ceb71 100644
--- a/src/OTP.php
+++ b/src/OTP.php
@@ -17,11 +17,12 @@ abstract class OTP implements OTPInterface
 {
     use ParameterTrait;
 
-    protected function __construct(null|string $secret, string $digest, int $digits)
+    /**
+     * @param non-empty-string $secret
+     */
+    protected function __construct(string $secret)
     {
         $this->setSecret($secret);
-        $this->setDigest($digest);
-        $this->setDigits($digits);
     }
 
     public function getQrCodeUri(string $uri, string $placeholder): string
diff --git a/src/OTPInterface.php b/src/OTPInterface.php
index 072e8212febf698af77319516a0df6f8ca4a35fc..3b27f54564106cb02e3115e43c8ea6d976b7f539 100644
--- a/src/OTPInterface.php
+++ b/src/OTPInterface.php
@@ -6,6 +6,34 @@ namespace OTPHP;
 
 interface OTPInterface
 {
+    public const DEFAULT_DIGITS = 6;
+
+    public const DEFAULT_DIGEST = 'sha1';
+
+    /**
+     * Create a OTP object from an existing secret.
+     *
+     * @param non-empty-string $secret
+     */
+    public static function createFromSecret(string $secret): self;
+
+    /**
+     * Create a new OTP object. A random 64 bytes secret will be generated.
+     */
+    public static function generate(): self;
+
+    /**
+     * @param non-empty-string $secret
+     */
+    public function setSecret(string $secret): void;
+
+    public function setDigits(int $digits): void;
+
+    /**
+     * @param non-empty-string $digest
+     */
+    public function setDigest(string $digest): void;
+
     /**
      * @return string Return the OTP at the specified timestamp
      */
diff --git a/src/ParameterTrait.php b/src/ParameterTrait.php
index 2a01ca657d6970d74e754c54911f823e5abcab4f..80d38888325ff7f09c97cceacd28f6d0e6d93f13 100644
--- a/src/ParameterTrait.php
+++ b/src/ParameterTrait.php
@@ -121,6 +121,21 @@ trait ParameterTrait
         }
     }
 
+    public function setSecret(string $secret): void
+    {
+        $this->setParameter('secret', $secret);
+    }
+
+    public function setDigits(int $digits): void
+    {
+        $this->setParameter('digits', $digits);
+    }
+
+    public function setDigest(string $digest): void
+    {
+        $this->setParameter('algorithm', $digest);
+    }
+
     /**
      * @return array<string, callable>
      */
@@ -161,21 +176,6 @@ trait ParameterTrait
         ];
     }
 
-    private function setSecret(null|string $secret): void
-    {
-        $this->setParameter('secret', $secret);
-    }
-
-    private function setDigits(int $digits): void
-    {
-        $this->setParameter('digits', $digits);
-    }
-
-    private function setDigest(string $digest): void
-    {
-        $this->setParameter('algorithm', $digest);
-    }
-
     private function hasColon(string $value): bool
     {
         $colons = [':', '%3A', '%3a'];
diff --git a/src/TOTP.php b/src/TOTP.php
index 61b81048a35dfd8c3ddd055d9ee4c767ae2c3f83..e9bce9e14084a03f1e32268296e523cf92a083f5 100644
--- a/src/TOTP.php
+++ b/src/TOTP.php
@@ -12,40 +12,39 @@ use function is_int;
  */
 final class TOTP extends OTP implements TOTPInterface
 {
-    protected function __construct(null|string $secret, int $period, string $digest, int $digits, int $epoch = 0)
-    {
-        parent::__construct($secret, $digest, $digits);
-        $this->setPeriod($period);
-        $this->setEpoch($epoch);
-    }
-
     public static function create(
         null|string $secret = null,
-        int $period = 30,
-        string $digest = 'sha1',
-        int $digits = 6,
-        int $epoch = 0
+        int $period = self::DEFAULT_PERIOD,
+        string $digest = self::DEFAULT_DIGEST,
+        int $digits = self::DEFAULT_DIGITS,
+        int $epoch = self::DEFAULT_EPOCH
     ): self {
-        return new self($secret, $period, $digest, $digits, $epoch);
+        $totp = $secret !== null
+            ? self::createFromSecret($secret)
+            : self::generate()
+        ;
+        $totp->setPeriod($period);
+        $totp->setDigest($digest);
+        $totp->setDigits($digits);
+        $totp->setEpoch($epoch);
+
+        return $totp;
     }
 
-    public static function createFromSecret(
-        string $secret,
-        int $period = 30,
-        string $digest = 'sha1',
-        int $digits = 6,
-        int $epoch = 0
-    ): self {
-        return new self($secret, $period, $digest, $digits, $epoch);
+    public static function createFromSecret(string $secret): self
+    {
+        $totp = new self($secret);
+        $totp->setPeriod(self::DEFAULT_PERIOD);
+        $totp->setDigest(self::DEFAULT_DIGEST);
+        $totp->setDigits(self::DEFAULT_DIGITS);
+        $totp->setEpoch(self::DEFAULT_EPOCH);
+
+        return $totp;
     }
 
-    public static function generate(
-        int $period = 30,
-        string $digest = 'sha1',
-        int $digits = 6,
-        int $epoch = 0
-    ): self {
-        return new self(self::generateSecret(), $period, $digest, $digits, $epoch);
+    public static function generate(): self
+    {
+        return self::createFromSecret(self::generateSecret());
     }
 
     public function getPeriod(): int
@@ -118,11 +117,16 @@ final class TOTP extends OTP implements TOTPInterface
         return $this->generateURI('totp', $params);
     }
 
-    protected function setPeriod(int $period): void
+    public function setPeriod(int $period): void
     {
         $this->setParameter('period', $period);
     }
 
+    public function setEpoch(int $epoch): void
+    {
+        $this->setParameter('epoch', $epoch);
+    }
+
     /**
      * @return array<string, callable>
      */
@@ -161,11 +165,6 @@ final class TOTP extends OTP implements TOTPInterface
         ksort($options);
     }
 
-    private function setEpoch(int $epoch): void
-    {
-        $this->setParameter('epoch', $epoch);
-    }
-
     private function timecode(int $timestamp): int
     {
         return (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod());
diff --git a/src/TOTPInterface.php b/src/TOTPInterface.php
index d0f05d3e4feff7389ffa25684fd3632d575ce1d2..afb54e8e92914ace16269e62c051090145c4cf12 100644
--- a/src/TOTPInterface.php
+++ b/src/TOTPInterface.php
@@ -6,36 +6,30 @@ namespace OTPHP;
 
 interface TOTPInterface extends OTPInterface
 {
+    public const DEFAULT_PERIOD = 30;
+
+    public const DEFAULT_EPOCH = 0;
+
     /**
      * Create a new TOTP object.
      *
      * If the secret is null, a random 64 bytes secret will be generated.
      *
+     * @param null|non-empty-string $secret
+     * @param non-empty-string $digest
+     *
      * @deprecated Deprecated since v11.1, use ::createFromSecret or ::generate instead
      */
     public static function create(
         null|string $secret = null,
-        int $period = 30,
-        string $digest = 'sha1',
-        int $digits = 6
+        int $period = self::DEFAULT_PERIOD,
+        string $digest = self::DEFAULT_DIGEST,
+        int $digits = self::DEFAULT_DIGITS
     ): self;
 
-    /**
-     * Create a TOTP object from an existing secret.
-     *
-     * @param non-empty-string $secret
-     */
-    public static function createFromSecret(
-        string $secret,
-        int $period = 30,
-        string $digest = 'sha1',
-        int $digits = 6
-    ): self;
+    public function setPeriod(int $period): void;
 
-    /**
-     * Create a new TOTP object. A random 64 bytes secret will be generated.
-     */
-    public static function generate(int $period = 30, string $digest = 'sha1', int $digits = 6): self;
+    public function setEpoch(int $epoch): void;
 
     /**
      * Return the TOTP at the current time.
diff --git a/tests/HOTPTest.php b/tests/HOTPTest.php
index e21c2d609c85ae4ecbd78cba2c3431e7e2747b8f..f9e8531225bfc9120c31e3105ecc2ed4fdd73abb 100644
--- a/tests/HOTPTest.php
+++ b/tests/HOTPTest.php
@@ -30,10 +30,10 @@ final class HOTPTest extends TestCase
      */
     public function issuerHasColon(): void
     {
+        $otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
+
         $this->expectException(InvalidArgumentException::class);
         $this->expectExceptionMessage('Issuer must not contain a colon.');
-        $otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', 0, 'sha512', 8);
-        $otp->setLabel('alice');
         $otp->setIssuer('foo%3Abar');
     }
 
@@ -42,10 +42,10 @@ final class HOTPTest extends TestCase
      */
     public function issuerHasColon2(): void
     {
+        $otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
+
         $this->expectException(InvalidArgumentException::class);
         $this->expectExceptionMessage('Issuer must not contain a colon.');
-        $otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', 0, 'sha512', 8);
-        $otp->setLabel('alice');
         $otp->setIssuer('foo%3abar');
     }
 
@@ -54,11 +54,11 @@ final class HOTPTest extends TestCase
      */
     public function labelHasColon(): void
     {
+        $otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
+
         $this->expectException(InvalidArgumentException::class);
         $this->expectExceptionMessage('Label must not contain a colon.');
-        $otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', 0, 'sha512', 8);
         $otp->setLabel('foo%3Abar');
-        $otp->getProvisioningUri();
     }
 
     /**
@@ -66,11 +66,11 @@ final class HOTPTest extends TestCase
      */
     public function labelHasColon2(): void
     {
+        $otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
+
         $this->expectException(InvalidArgumentException::class);
         $this->expectExceptionMessage('Label must not contain a colon.');
-        $otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', 0, 'sha512', 8);
         $otp->setLabel('foo:bar');
-        $otp->getProvisioningUri();
     }
 
     /**
@@ -78,9 +78,11 @@ final class HOTPTest extends TestCase
      */
     public function digitsIsNot1OrMore(): void
     {
+        $htop = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
+
         $this->expectException(InvalidArgumentException::class);
         $this->expectExceptionMessage('Digits must be at least 1.');
-        HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', 0, 'sha512', 0);
+        $htop->setDigits(0);
     }
 
     /**
@@ -88,9 +90,11 @@ final class HOTPTest extends TestCase
      */
     public function counterIsNot1OrMore(): void
     {
+        $htop = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
+
         $this->expectException(InvalidArgumentException::class);
         $this->expectExceptionMessage('Counter must be at least 0.');
-        HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', -500);
+        $htop->setCounter(-500);
     }
 
     /**
@@ -98,9 +102,11 @@ final class HOTPTest extends TestCase
      */
     public function digestIsNotSupported(): void
     {
+        $htop = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
+
         $this->expectException(InvalidArgumentException::class);
         $this->expectExceptionMessage('The "foo" digest is not supported.');
-        HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', 0, 'foo');
+        $htop->setDigest('foo');
     }
 
     /**
@@ -110,11 +116,10 @@ final class HOTPTest extends TestCase
      */
     public function secretShouldBeBase32Encoded(): void
     {
+        $otp = HOTP::createFromSecret(random_bytes(32));
+
         $this->expectException(RuntimeException::class);
         $this->expectExceptionMessage('Unable to decode the secret. Is it correctly base32 encoded?');
-        $secret = random_bytes(32);
-
-        $otp = HOTP::createFromSecret($secret);
         $otp->at(0);
     }
 
@@ -185,8 +190,12 @@ final class HOTPTest extends TestCase
         string $issuer = 'My Project'
     ): HOTP {
         static::assertNotSame('', $secret);
+        static::assertNotSame('', $digest);
 
-        $otp = HOTP::createFromSecret($secret, $counter, $digest, $digits);
+        $otp = HOTP::createFromSecret($secret);
+        $otp->setCounter($counter);
+        $otp->setDigest($digest);
+        $otp->setDigits($digits);
         $otp->setLabel($label);
         $otp->setIssuer($issuer);
 
diff --git a/tests/TOTPTest.php b/tests/TOTPTest.php
index f28b7cb3b5aae839f057227babd6b8d817881335..03691da92b97e956bdec01bb42c0fbb5ca89e5e0 100644
--- a/tests/TOTPTest.php
+++ b/tests/TOTPTest.php
@@ -33,7 +33,11 @@ final class TOTPTest extends TestCase
      */
     public function customParameter(): void
     {
-        $otp = TOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', 20, 'sha512', 8, 100);
+        $otp = TOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
+        $otp->setPeriod(20);
+        $otp->setDigest('sha512');
+        $otp->setDigits(8);
+        $otp->setEpoch(100);
         $otp->setLabel('alice@foo.bar');
         $otp->setIssuer('My Project');
         $otp->setParameter('foo', 'bar.baz');
@@ -59,9 +63,11 @@ final class TOTPTest extends TestCase
      */
     public function periodIsNot1OrMore(): void
     {
+        $totp = TOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
+
         $this->expectException(InvalidArgumentException::class);
         $this->expectExceptionMessage('Period must be at least 1.');
-        TOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', -20, 'sha512', 8);
+        $totp->setPeriod(-20);
     }
 
     /**
@@ -69,9 +75,11 @@ final class TOTPTest extends TestCase
      */
     public function epochIsNot0OrMore(): void
     {
+        $totp = TOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
+
         $this->expectException(InvalidArgumentException::class);
         $this->expectExceptionMessage('Epoch must be greater than or equal to 0.');
-        TOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', 30, 'sha512', 8, -1);
+        $totp->setEpoch(-1);
     }
 
     /**
@@ -403,8 +411,13 @@ final class TOTPTest extends TestCase
         int $epoch = 0
     ): TOTP {
         static::assertNotSame('', $secret);
+        static::assertNotSame('', $digest);
 
-        $otp = TOTP::createFromSecret($secret, $period, $digest, $digits, $epoch);
+        $otp = TOTP::createFromSecret($secret);
+        $otp->setPeriod($period);
+        $otp->setDigest($digest);
+        $otp->setDigits($digits);
+        $otp->setEpoch($epoch);
         $otp->setLabel($label);
         $otp->setIssuer($issuer);