diff --git a/signet.cabal b/signet.cabal
--- a/signet.cabal
+++ b/signet.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.0
 name: signet
-version: 0.2025.5.9
+version: 0.2025.5.12
 synopsis: Standard Webhooks
 description:
   Signet is a Haskell library for working with Standard Webhooks.
@@ -32,6 +32,7 @@
     -Wno-missing-deriving-strategies
     -Wno-missing-export-lists
     -Wno-missing-kind-signatures
+    -Wno-missing-role-annotations
     -Wno-missing-safe-haskell-mode
     -Wno-prepositive-qualified-module
     -Wno-safe
@@ -40,127 +41,103 @@
   if flag(pedantic)
     ghc-options: -Werror
 
-common executable
+library
   import: library
-  ghc-options:
-    -rtsopts
-    -threaded
-
-common dependencies
   build-depends:
     bytestring ^>=0.12,
     case-insensitive ^>=1.2.1.0,
     crypton ^>=0.34 || ^>=1.0.4,
     exceptions ^>=0.10.7,
-    memory ^>=0.18.0,
-    time ^>=1.12.2,
-
-library unstable
-  import: library, dependencies
-  build-depends:
     http-types ^>=0.12.4,
+    memory ^>=0.18.0,
     text ^>=2.0 || ^>=2.1,
+    time ^>=1.12.2,
 
-  -- cabal-gild: discover source/libraries/unstable
+  -- cabal-gild: discover source/library
   exposed-modules:
+    Signet
     Signet.Unstable
     Signet.Unstable.Exception.InvalidAsymmetricSignature
+    Signet.Unstable.Exception.InvalidAsymmetricSignatureTest
     Signet.Unstable.Exception.InvalidId
+    Signet.Unstable.Exception.InvalidIdTest
     Signet.Unstable.Exception.InvalidMessage
+    Signet.Unstable.Exception.InvalidMessageTest
     Signet.Unstable.Exception.InvalidPublicKey
+    Signet.Unstable.Exception.InvalidPublicKeyTest
     Signet.Unstable.Exception.InvalidSecret
     Signet.Unstable.Exception.InvalidSecretKey
-    Signet.Unstable.Exception.InvalidSignature
-    Signet.Unstable.Exception.InvalidSigner
-    Signet.Unstable.Exception.InvalidSymmetricSignature
-    Signet.Unstable.Exception.InvalidTimestamp
-    Signet.Unstable.Exception.InvalidVerifier
-    Signet.Unstable.Exception.SignetException
-    Signet.Unstable.Exception.ToleranceException
-    Signet.Unstable.Exception.VerificationException
-    Signet.Unstable.Extra.Either
-    Signet.Unstable.Extra.Http
-    Signet.Unstable.Extra.Maybe
-    Signet.Unstable.Type.AsymmetricSignature
-    Signet.Unstable.Type.Id
-    Signet.Unstable.Type.Message
-    Signet.Unstable.Type.Payload
-    Signet.Unstable.Type.PublicKey
-    Signet.Unstable.Type.Secret
-    Signet.Unstable.Type.SecretKey
-    Signet.Unstable.Type.Signature
-    Signet.Unstable.Type.Signatures
-    Signet.Unstable.Type.Signer
-    Signet.Unstable.Type.SymmetricSignature
-    Signet.Unstable.Type.Timestamp
-    Signet.Unstable.Type.Tolerance
-    Signet.Unstable.Type.UnknownSignature
-    Signet.Unstable.Type.Verifier
-
-  hs-source-dirs: source/libraries/unstable
-  visibility: public
-
-library
-  import: library
-  build-depends: signet:unstable
-  -- cabal-gild: discover source/libraries/signet
-  exposed-modules: Signet
-  hs-source-dirs: source/libraries/signet
-
-library test
-  import: library, dependencies
-  build-depends:
-    signet:unstable,
-    tasty ^>=1.4 || ^>=1.5.3,
-    tasty-hunit ^>=0.10.2,
-    transformers ^>=0.6.1.0,
-
-  -- cabal-gild: discover source/libraries/test
-  exposed-modules:
-    Signet.Unstable.Exception.InvalidAsymmetricSignatureTest
-    Signet.Unstable.Exception.InvalidIdTest
-    Signet.Unstable.Exception.InvalidMessageTest
-    Signet.Unstable.Exception.InvalidPublicKeyTest
     Signet.Unstable.Exception.InvalidSecretKeyTest
     Signet.Unstable.Exception.InvalidSecretTest
+    Signet.Unstable.Exception.InvalidSignature
     Signet.Unstable.Exception.InvalidSignatureTest
+    Signet.Unstable.Exception.InvalidSigner
     Signet.Unstable.Exception.InvalidSignerTest
+    Signet.Unstable.Exception.InvalidSymmetricSignature
     Signet.Unstable.Exception.InvalidSymmetricSignatureTest
+    Signet.Unstable.Exception.InvalidTimestamp
     Signet.Unstable.Exception.InvalidTimestampTest
+    Signet.Unstable.Exception.InvalidVerifier
     Signet.Unstable.Exception.InvalidVerifierTest
+    Signet.Unstable.Exception.SignetException
     Signet.Unstable.Exception.SignetExceptionTest
+    Signet.Unstable.Exception.ToleranceException
     Signet.Unstable.Exception.ToleranceExceptionTest
+    Signet.Unstable.Exception.VerificationException
     Signet.Unstable.Exception.VerificationExceptionTest
+    Signet.Unstable.Extra.Either
     Signet.Unstable.Extra.EitherTest
+    Signet.Unstable.Extra.Http
     Signet.Unstable.Extra.HttpTest
+    Signet.Unstable.Extra.Maybe
     Signet.Unstable.Extra.MaybeTest
-    Signet.Unstable.Extra.Tasty
+    Signet.Unstable.Type.AsymmetricSignature
     Signet.Unstable.Type.AsymmetricSignatureTest
+    Signet.Unstable.Type.Id
     Signet.Unstable.Type.IdTest
+    Signet.Unstable.Type.Message
     Signet.Unstable.Type.MessageTest
+    Signet.Unstable.Type.Payload
     Signet.Unstable.Type.PayloadTest
+    Signet.Unstable.Type.PublicKey
     Signet.Unstable.Type.PublicKeyTest
+    Signet.Unstable.Type.Secret
+    Signet.Unstable.Type.SecretKey
     Signet.Unstable.Type.SecretKeyTest
     Signet.Unstable.Type.SecretTest
+    Signet.Unstable.Type.Signature
     Signet.Unstable.Type.SignatureTest
+    Signet.Unstable.Type.Signatures
     Signet.Unstable.Type.SignaturesTest
+    Signet.Unstable.Type.Signer
     Signet.Unstable.Type.SignerTest
+    Signet.Unstable.Type.SymmetricSignature
     Signet.Unstable.Type.SymmetricSignatureTest
+    Signet.Unstable.Type.Test
+    Signet.Unstable.Type.Timestamp
     Signet.Unstable.Type.TimestampTest
+    Signet.Unstable.Type.Tolerance
     Signet.Unstable.Type.ToleranceTest
+    Signet.Unstable.Type.UnknownSignature
     Signet.Unstable.Type.UnknownSignatureTest
+    Signet.Unstable.Type.Verifier
     Signet.Unstable.Type.VerifierTest
     Signet.UnstableTest
     SignetTest
 
-  hs-source-dirs: source/libraries/test
-  visibility: public
+  hs-source-dirs: source/library
 
 test-suite signet-test-suite
-  import: executable
+  import: library
   build-depends:
-    signet:test,
-    tasty,
+    signet,
+    tasty ^>=1.4 || ^>=1.5.3,
+    tasty-hunit ^>=0.10.2,
+    transformers ^>=0.6.1.0,
+
+  ghc-options:
+    -rtsopts
+    -threaded
 
   hs-source-dirs: source/test-suite
   main-is: Main.hs
diff --git a/source/libraries/signet/Signet.hs b/source/libraries/signet/Signet.hs
deleted file mode 100644
--- a/source/libraries/signet/Signet.hs
+++ /dev/null
@@ -1,88 +0,0 @@
-module Signet
-  ( -- * Verification
-    Signet.Unstable.verifyWebhookText,
-    Signet.Unstable.verifyWebhookByteString,
-    Signet.Unstable.verifyWebhook,
-    Signet.Unstable.verifyWebhookWith,
-
-    -- * Signing
-    Signet.Unstable.signWebhook,
-
-    -- * Headers
-    Signet.Unstable.Extra.Http.hWebhookId,
-    Signet.Unstable.Extra.Http.hWebhookTimestamp,
-    Signet.Unstable.Extra.Http.hWebhookSignature,
-
-    -- * Types
-    Signet.Unstable.Type.AsymmetricSignature.AsymmetricSignature (..),
-    Signet.Unstable.Type.Id.Id (..),
-    Signet.Unstable.parseId,
-    Signet.Unstable.Type.Message.Message (..),
-    Signet.Unstable.parseMessage,
-    Signet.Unstable.Type.Payload.Payload (..),
-    Signet.Unstable.Type.PublicKey.PublicKey (..),
-    Signet.Unstable.Type.Secret.Secret (..),
-    Signet.Unstable.Type.SecretKey.SecretKey (..),
-    Signet.Unstable.Type.Signature.Signature (Signet.Unstable.AsymmetricSignature, Signet.Unstable.SymmetricSignature),
-    Signet.Unstable.Type.Signatures.Signatures (..),
-    Signet.Unstable.parseSignatures,
-    Signet.Unstable.Type.Signer.Signer (Signet.Unstable.AsymmetricSigner, Signet.Unstable.SymmetricSigner),
-    Signet.Unstable.parseSigner,
-    Signet.Unstable.Type.SymmetricSignature.SymmetricSignature (..),
-    Signet.Unstable.Type.Timestamp.Timestamp (..),
-    Signet.Unstable.parseTimestamp,
-    Signet.Unstable.Type.Tolerance.Tolerance (..),
-    Signet.Unstable.typicalTolerance,
-    Signet.Unstable.Type.UnknownSignature.UnknownSignature (..),
-    Signet.Unstable.Type.Verifier.Verifier (Signet.Unstable.AsymmetricVerifier, Signet.Unstable.SymmetricVerifier),
-    Signet.Unstable.parseVerifier,
-
-    -- * Exceptions
-    Signet.Unstable.Exception.InvalidAsymmetricSignature.InvalidAsymmetricSignature (..),
-    Signet.Unstable.Exception.InvalidId.InvalidId (..),
-    Signet.Unstable.Exception.InvalidMessage.InvalidMessage (..),
-    Signet.Unstable.Exception.InvalidPublicKey.InvalidPublicKey (..),
-    Signet.Unstable.Exception.InvalidSecret.InvalidSecret (..),
-    Signet.Unstable.Exception.InvalidSecretKey.InvalidSecretKey (..),
-    Signet.Unstable.Exception.InvalidSignature.InvalidSignature (..),
-    Signet.Unstable.Exception.InvalidSigner.InvalidSigner (..),
-    Signet.Unstable.Exception.InvalidSymmetricSignature.InvalidSymmetricSignature (..),
-    Signet.Unstable.Exception.InvalidTimestamp.InvalidTimestamp (..),
-    Signet.Unstable.Exception.InvalidVerifier.InvalidVerifier (..),
-    Signet.Unstable.Exception.SignetException.SignetException (..),
-    Signet.Unstable.Exception.ToleranceException.ToleranceException (..),
-    Signet.Unstable.Exception.VerificationException.VerificationException (..),
-  )
-where
-
-import qualified Signet.Unstable
-import qualified Signet.Unstable.Exception.InvalidAsymmetricSignature
-import qualified Signet.Unstable.Exception.InvalidId
-import qualified Signet.Unstable.Exception.InvalidMessage
-import qualified Signet.Unstable.Exception.InvalidPublicKey
-import qualified Signet.Unstable.Exception.InvalidSecret
-import qualified Signet.Unstable.Exception.InvalidSecretKey
-import qualified Signet.Unstable.Exception.InvalidSignature
-import qualified Signet.Unstable.Exception.InvalidSigner
-import qualified Signet.Unstable.Exception.InvalidSymmetricSignature
-import qualified Signet.Unstable.Exception.InvalidTimestamp
-import qualified Signet.Unstable.Exception.InvalidVerifier
-import qualified Signet.Unstable.Exception.SignetException
-import qualified Signet.Unstable.Exception.ToleranceException
-import qualified Signet.Unstable.Exception.VerificationException
-import qualified Signet.Unstable.Extra.Http
-import qualified Signet.Unstable.Type.AsymmetricSignature
-import qualified Signet.Unstable.Type.Id
-import qualified Signet.Unstable.Type.Message
-import qualified Signet.Unstable.Type.Payload
-import qualified Signet.Unstable.Type.PublicKey
-import qualified Signet.Unstable.Type.Secret
-import qualified Signet.Unstable.Type.SecretKey
-import qualified Signet.Unstable.Type.Signature
-import qualified Signet.Unstable.Type.Signatures
-import qualified Signet.Unstable.Type.Signer
-import qualified Signet.Unstable.Type.SymmetricSignature
-import qualified Signet.Unstable.Type.Timestamp
-import qualified Signet.Unstable.Type.Tolerance
-import qualified Signet.Unstable.Type.UnknownSignature
-import qualified Signet.Unstable.Type.Verifier
diff --git a/source/libraries/test/Signet/Unstable/Exception/InvalidAsymmetricSignatureTest.hs b/source/libraries/test/Signet/Unstable/Exception/InvalidAsymmetricSignatureTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Exception/InvalidAsymmetricSignatureTest.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Signet.Unstable.Exception.InvalidAsymmetricSignatureTest where
-
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Exception.InvalidAsymmetricSignature" $ do
-  pure ()
diff --git a/source/libraries/test/Signet/Unstable/Exception/InvalidIdTest.hs b/source/libraries/test/Signet/Unstable/Exception/InvalidIdTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Exception/InvalidIdTest.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Signet.Unstable.Exception.InvalidIdTest where
-
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Exception.InvalidId" $ do
-  pure ()
diff --git a/source/libraries/test/Signet/Unstable/Exception/InvalidMessageTest.hs b/source/libraries/test/Signet/Unstable/Exception/InvalidMessageTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Exception/InvalidMessageTest.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Signet.Unstable.Exception.InvalidMessageTest where
-
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Exception.InvalidMessage" $ do
-  pure ()
diff --git a/source/libraries/test/Signet/Unstable/Exception/InvalidPublicKeyTest.hs b/source/libraries/test/Signet/Unstable/Exception/InvalidPublicKeyTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Exception/InvalidPublicKeyTest.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Signet.Unstable.Exception.InvalidPublicKeyTest where
-
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Exception.InvalidPublicKey" $ do
-  pure ()
diff --git a/source/libraries/test/Signet/Unstable/Exception/InvalidSecretKeyTest.hs b/source/libraries/test/Signet/Unstable/Exception/InvalidSecretKeyTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Exception/InvalidSecretKeyTest.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Signet.Unstable.Exception.InvalidSecretKeyTest where
-
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Exception.InvalidSecretKey" $ do
-  pure ()
diff --git a/source/libraries/test/Signet/Unstable/Exception/InvalidSecretTest.hs b/source/libraries/test/Signet/Unstable/Exception/InvalidSecretTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Exception/InvalidSecretTest.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Signet.Unstable.Exception.InvalidSecretTest where
-
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Exception.InvalidSecret" $ do
-  pure ()
diff --git a/source/libraries/test/Signet/Unstable/Exception/InvalidSignatureTest.hs b/source/libraries/test/Signet/Unstable/Exception/InvalidSignatureTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Exception/InvalidSignatureTest.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Signet.Unstable.Exception.InvalidSignatureTest where
-
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Exception.InvalidSignature" $ do
-  pure ()
diff --git a/source/libraries/test/Signet/Unstable/Exception/InvalidSignerTest.hs b/source/libraries/test/Signet/Unstable/Exception/InvalidSignerTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Exception/InvalidSignerTest.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Signet.Unstable.Exception.InvalidSignerTest where
-
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Exception.InvalidSigner" $ do
-  pure ()
diff --git a/source/libraries/test/Signet/Unstable/Exception/InvalidSymmetricSignatureTest.hs b/source/libraries/test/Signet/Unstable/Exception/InvalidSymmetricSignatureTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Exception/InvalidSymmetricSignatureTest.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Signet.Unstable.Exception.InvalidSymmetricSignatureTest where
-
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Exception.InvalidSymmetricSignature" $ do
-  pure ()
diff --git a/source/libraries/test/Signet/Unstable/Exception/InvalidTimestampTest.hs b/source/libraries/test/Signet/Unstable/Exception/InvalidTimestampTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Exception/InvalidTimestampTest.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Signet.Unstable.Exception.InvalidTimestampTest where
-
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Exception.InvalidTimestamp" $ do
-  pure ()
diff --git a/source/libraries/test/Signet/Unstable/Exception/InvalidVerifierTest.hs b/source/libraries/test/Signet/Unstable/Exception/InvalidVerifierTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Exception/InvalidVerifierTest.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Signet.Unstable.Exception.InvalidVerifierTest where
-
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Exception.InvalidVerifier" $ do
-  pure ()
diff --git a/source/libraries/test/Signet/Unstable/Exception/SignetExceptionTest.hs b/source/libraries/test/Signet/Unstable/Exception/SignetExceptionTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Exception/SignetExceptionTest.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Signet.Unstable.Exception.SignetExceptionTest where
-
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Exception.SignetException" $ do
-  pure ()
diff --git a/source/libraries/test/Signet/Unstable/Exception/ToleranceExceptionTest.hs b/source/libraries/test/Signet/Unstable/Exception/ToleranceExceptionTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Exception/ToleranceExceptionTest.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Signet.Unstable.Exception.ToleranceExceptionTest where
-
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Exception.ToleranceException" $ do
-  pure ()
diff --git a/source/libraries/test/Signet/Unstable/Exception/VerificationExceptionTest.hs b/source/libraries/test/Signet/Unstable/Exception/VerificationExceptionTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Exception/VerificationExceptionTest.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Signet.Unstable.Exception.VerificationExceptionTest where
-
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Exception.VerificationException" $ do
-  pure ()
diff --git a/source/libraries/test/Signet/Unstable/Extra/EitherTest.hs b/source/libraries/test/Signet/Unstable/Extra/EitherTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Extra/EitherTest.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-module Signet.Unstable.Extra.EitherTest where
-
-import qualified Control.Monad.Catch as Exception
-import qualified Data.Void as Void
-import qualified Signet.Unstable.Extra.Either as Either
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-import Test.Tasty.HUnit ((@?=))
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Extra.Either" $ do
-  Tasty.describe "hush" $ do
-    Tasty.it "works with Left" $ do
-      Either.hush (Left () :: Either () Void.Void) @?= Nothing
-
-    Tasty.it "works with Right" $ do
-      Either.hush (Right () :: Either Void.Void ()) @?= Just ()
-
-  Tasty.describe "throw" $ do
-    Tasty.it "throws an exception for Left" $ do
-      result <- Exception.try . Either.throw $ Left MkTestException
-      result @?= (Left MkTestException :: Either TestException Void.Void)
-
-    Tasty.it "returns the value for Right" $ do
-      value <- Either.throw (Right () :: Either Void.Void ())
-      value @?= ()
-
-data TestException
-  = MkTestException
-  deriving (Eq, Show)
-
-instance Exception.Exception TestException
diff --git a/source/libraries/test/Signet/Unstable/Extra/HttpTest.hs b/source/libraries/test/Signet/Unstable/Extra/HttpTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Extra/HttpTest.hs
+++ /dev/null
@@ -1,21 +0,0 @@
-module Signet.Unstable.Extra.HttpTest where
-
-import qualified Data.ByteString.Char8 as Ascii
-import qualified Data.CaseInsensitive as CI
-import qualified Signet.Unstable.Extra.Http as Http
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-import Test.Tasty.HUnit ((@?=))
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Extra.Http" $ do
-  Tasty.describe "hWebhookId" $ do
-    Tasty.it "creates the correct header name" $ do
-      Http.hWebhookId @?= CI.mk (Ascii.pack "webhook-id")
-
-  Tasty.describe "hWebhookSignature" $ do
-    Tasty.it "creates the correct header name" $ do
-      Http.hWebhookSignature @?= CI.mk (Ascii.pack "webhook-signature")
-
-  Tasty.describe "hWebhookTimestamp" $ do
-    Tasty.it "creates the correct header name" $ do
-      Http.hWebhookTimestamp @?= CI.mk (Ascii.pack "webhook-timestamp")
diff --git a/source/libraries/test/Signet/Unstable/Extra/MaybeTest.hs b/source/libraries/test/Signet/Unstable/Extra/MaybeTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Extra/MaybeTest.hs
+++ /dev/null
@@ -1,15 +0,0 @@
-module Signet.Unstable.Extra.MaybeTest where
-
-import qualified Data.Void as Void
-import qualified Signet.Unstable.Extra.Maybe as Maybe
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-import Test.Tasty.HUnit ((@?=))
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Extra.Maybe" $ do
-  Tasty.describe "note" $ do
-    Tasty.it "works with nothing" $ do
-      Maybe.note () (Nothing :: Maybe Void.Void) @?= Left ()
-
-    Tasty.it "works with just" $ do
-      Maybe.note () (Just True) @?= Right True
diff --git a/source/libraries/test/Signet/Unstable/Extra/Tasty.hs b/source/libraries/test/Signet/Unstable/Extra/Tasty.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Extra/Tasty.hs
+++ /dev/null
@@ -1,13 +0,0 @@
-module Signet.Unstable.Extra.Tasty where
-
-import qualified Control.Monad.Trans.Writer as Writer
-import qualified Test.Tasty as Tasty
-import qualified Test.Tasty.HUnit as Tasty
-
-type Spec = Writer.Writer [Tasty.TestTree] ()
-
-describe :: Tasty.TestName -> Spec -> Spec
-describe testName = Writer.tell . pure . Tasty.testGroup testName . Writer.execWriter
-
-it :: Tasty.TestName -> Tasty.Assertion -> Spec
-it testName = Writer.tell . pure . Tasty.testCase testName
diff --git a/source/libraries/test/Signet/Unstable/Type/AsymmetricSignatureTest.hs b/source/libraries/test/Signet/Unstable/Type/AsymmetricSignatureTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Type/AsymmetricSignatureTest.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-module Signet.Unstable.Type.AsymmetricSignatureTest where
-
-import qualified Crypto.Error as Error
-import qualified Crypto.PubKey.Ed25519 as Ed25519
-import qualified Data.ByteString.Char8 as Ascii
-import qualified Signet.Unstable.Exception.InvalidAsymmetricSignature as InvalidAsymmetricSignature
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-import qualified Signet.Unstable.Type.AsymmetricSignature as AsymmetricSignature
-import Test.Tasty.HUnit ((@?=))
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Type.AsymmetricSignature" $ do
-  Tasty.describe "parse" $ do
-    Tasty.it "fails with invalid input" $ do
-      let byteString = Ascii.pack "invalid"
-      let result = AsymmetricSignature.parse byteString
-      result @?= Left (InvalidAsymmetricSignature.MkInvalidAsymmetricSignature byteString)
-
-    Tasty.it "succeeds with valid input" $ do
-      let result = AsymmetricSignature.parse $ Ascii.pack "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVotMDEyMzQ1Njc4OS1hYmNkZWZnaGlqa2xtbm9wcXJzdHV2cXh5eg=="
-      signature <- Error.throwCryptoErrorIO . Ed25519.signature $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789-abcdefghijklmnopqrstuvqxyz"
-      result @?= Right (AsymmetricSignature.MkAsymmetricSignature signature)
-
-  Tasty.describe "render" $ do
-    Tasty.it "works" $ do
-      asymmetricSignature <- fmap AsymmetricSignature.MkAsymmetricSignature . Error.throwCryptoErrorIO . Ed25519.signature $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789-abcdefghijklmnopqrstuvqxyz"
-      AsymmetricSignature.render asymmetricSignature @?= Ascii.pack "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVotMDEyMzQ1Njc4OS1hYmNkZWZnaGlqa2xtbm9wcXJzdHV2cXh5eg=="
diff --git a/source/libraries/test/Signet/Unstable/Type/IdTest.hs b/source/libraries/test/Signet/Unstable/Type/IdTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Type/IdTest.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-module Signet.Unstable.Type.IdTest where
-
-import qualified Data.ByteString.Char8 as Ascii
-import qualified Signet.Unstable.Exception.InvalidId as InvalidId
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-import qualified Signet.Unstable.Type.Id as Id
-import Test.Tasty.HUnit ((@?=))
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Type.Id" $ do
-  Tasty.describe "parse" $ do
-    Tasty.it "fails with input containing separator" $ do
-      let byteString = Ascii.pack "invalid.id"
-      let result = Id.parse byteString
-      result @?= Left (InvalidId.MkInvalidId byteString)
-
-    Tasty.it "succeeds with valid input" $ do
-      let byteString = Ascii.pack "valid-id"
-      let result = Id.parse byteString
-      result @?= Right (Id.MkId byteString)
-
-  Tasty.describe "render" $ do
-    Tasty.it "returns the original ByteString" $ do
-      let byteString = Ascii.pack "valid-id"
-      let id_ = Id.MkId byteString
-      Id.render id_ @?= byteString
diff --git a/source/libraries/test/Signet/Unstable/Type/MessageTest.hs b/source/libraries/test/Signet/Unstable/Type/MessageTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Type/MessageTest.hs
+++ /dev/null
@@ -1,47 +0,0 @@
-module Signet.Unstable.Type.MessageTest where
-
-import qualified Data.ByteString.Char8 as Ascii
-import qualified Signet.Unstable.Exception.InvalidMessage as InvalidMessage
-import qualified Signet.Unstable.Exception.InvalidTimestamp as InvalidTimestamp
-import qualified Signet.Unstable.Extra.Either as Either
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-import qualified Signet.Unstable.Type.Id as Id
-import qualified Signet.Unstable.Type.Message as Message
-import qualified Signet.Unstable.Type.Payload as Payload
-import qualified Signet.Unstable.Type.Timestamp as Timestamp
-import Test.Tasty.HUnit ((@?=))
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Type.Message" $ do
-  Tasty.describe "parse" $ do
-    Tasty.it "fails with invalid timestamp" $ do
-      let timestamp = Ascii.pack "invalid"
-      let byteString = Ascii.pack "i." <> timestamp <> Ascii.pack ".p"
-      let result = Message.parse byteString
-      result @?= Left (InvalidMessage.InvalidTimestamp $ InvalidTimestamp.MkInvalidTimestamp timestamp)
-
-    Tasty.it "succeeds with valid input" $ do
-      let result = Message.parse $ Ascii.pack "i.0.p"
-      id_ <- Either.throw . Id.parse $ Ascii.pack "i"
-      timestamp <- Either.throw . Timestamp.parse $ Ascii.pack "0"
-      let payload = Payload.MkPayload $ Ascii.pack "p"
-      result
-        @?= Right
-          Message.MkMessage
-            { Message.id_ = id_,
-              Message.timestamp = timestamp,
-              Message.payload = payload
-            }
-
-  Tasty.describe "render" $ do
-    Tasty.it "returns the correct ByteString representation" $ do
-      id_ <- Either.throw . Id.parse $ Ascii.pack "i"
-      timestamp <- Either.throw . Timestamp.parse $ Ascii.pack "0"
-      let payload = Payload.MkPayload $ Ascii.pack "p"
-      let message =
-            Message.MkMessage
-              { Message.id_ = id_,
-                Message.timestamp = timestamp,
-                Message.payload = payload
-              }
-      Message.render message @?= Ascii.pack "i.0.p"
diff --git a/source/libraries/test/Signet/Unstable/Type/PayloadTest.hs b/source/libraries/test/Signet/Unstable/Type/PayloadTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Type/PayloadTest.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Signet.Unstable.Type.PayloadTest where
-
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Type.Payload" $ do
-  pure ()
diff --git a/source/libraries/test/Signet/Unstable/Type/PublicKeyTest.hs b/source/libraries/test/Signet/Unstable/Type/PublicKeyTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Type/PublicKeyTest.hs
+++ /dev/null
@@ -1,32 +0,0 @@
-module Signet.Unstable.Type.PublicKeyTest where
-
-import qualified Crypto.Error as Error
-import qualified Crypto.PubKey.Ed25519 as Ed25519
-import qualified Data.ByteString.Char8 as Ascii
-import qualified Signet.Unstable.Exception.InvalidPublicKey as InvalidPublicKey
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-import qualified Signet.Unstable.Type.PublicKey as PublicKey
-import Test.Tasty.HUnit ((@?=))
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Type.PublicKey" $ do
-  Tasty.describe "parse" $ do
-    Tasty.it "fails with invalid prefix" $ do
-      let byteString = Ascii.pack "invalid"
-      let result = PublicKey.parse byteString
-      result @?= Left (InvalidPublicKey.MkInvalidPublicKey byteString)
-
-    Tasty.it "fails with invalid input" $ do
-      let byteString = Ascii.pack "whpk_invalid"
-      let result = PublicKey.parse byteString
-      result @?= Left (InvalidPublicKey.MkInvalidPublicKey byteString)
-
-    Tasty.it "succeeds with valid input" $ do
-      let result = PublicKey.parse $ Ascii.pack "whpk_QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU="
-      publicKey <- fmap PublicKey.MkPublicKey . Error.throwCryptoErrorIO . Ed25519.publicKey $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"
-      result @?= Right publicKey
-
-  Tasty.describe "render" $ do
-    Tasty.it "works" $ do
-      publicKey <- fmap PublicKey.MkPublicKey . Error.throwCryptoErrorIO . Ed25519.publicKey $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"
-      PublicKey.render publicKey @?= Ascii.pack "whpk_QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU="
diff --git a/source/libraries/test/Signet/Unstable/Type/SecretKeyTest.hs b/source/libraries/test/Signet/Unstable/Type/SecretKeyTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Type/SecretKeyTest.hs
+++ /dev/null
@@ -1,32 +0,0 @@
-module Signet.Unstable.Type.SecretKeyTest where
-
-import qualified Crypto.Error as Error
-import qualified Crypto.PubKey.Ed25519 as Ed25519
-import qualified Data.ByteString.Char8 as Ascii
-import qualified Signet.Unstable.Exception.InvalidSecretKey as InvalidSecretKey
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-import qualified Signet.Unstable.Type.SecretKey as SecretKey
-import Test.Tasty.HUnit ((@?=))
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Type.SecretKey" $ do
-  Tasty.describe "parse" $ do
-    Tasty.it "fails with invalid prefix" $ do
-      let byteString = Ascii.pack "invalid"
-      let result = SecretKey.parse byteString
-      result @?= Left (InvalidSecretKey.MkInvalidSecretKey byteString)
-
-    Tasty.it "fails with invalid input" $ do
-      let byteString = Ascii.pack "whsk_invalid"
-      let result = SecretKey.parse byteString
-      result @?= Left (InvalidSecretKey.MkInvalidSecretKey byteString)
-
-    Tasty.it "succeeds with valid input" $ do
-      let result = SecretKey.parse $ Ascii.pack "whsk_QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU="
-      secretKey <- fmap SecretKey.MkSecretKey . Error.throwCryptoErrorIO . Ed25519.secretKey $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"
-      result @?= Right secretKey
-
-  Tasty.describe "render" $ do
-    Tasty.it "works" $ do
-      secretKey <- fmap SecretKey.MkSecretKey . Error.throwCryptoErrorIO . Ed25519.secretKey $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"
-      SecretKey.render secretKey @?= Ascii.pack "whsk_QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU="
diff --git a/source/libraries/test/Signet/Unstable/Type/SecretTest.hs b/source/libraries/test/Signet/Unstable/Type/SecretTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Type/SecretTest.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-module Signet.Unstable.Type.SecretTest where
-
-import qualified Data.ByteArray as ByteArray
-import qualified Data.ByteString.Char8 as Ascii
-import qualified Signet.Unstable.Exception.InvalidSecret as InvalidSecret
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-import qualified Signet.Unstable.Type.Secret as Secret
-import Test.Tasty.HUnit ((@?=))
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Type.Secret" $ do
-  Tasty.describe "parse" $ do
-    Tasty.it "fails with invalid prefix" $ do
-      let byteString = Ascii.pack "invalid"
-      let result = Secret.parse byteString
-      result @?= Left (InvalidSecret.MkInvalidSecret byteString)
-
-    Tasty.it "fails with invalid input" $ do
-      let byteString = Ascii.pack "whsec_invalid"
-      let result = Secret.parse byteString
-      result @?= Left (InvalidSecret.MkInvalidSecret byteString)
-
-    Tasty.it "succeeds with valid input" $ do
-      let result = Secret.parse $ Ascii.pack "whsec_MDEyMzQ1Njc4OQ=="
-      let secret = Secret.MkSecret . ByteArray.convert $ Ascii.pack "0123456789"
-      result @?= Right secret
-
-  Tasty.describe "render" $ do
-    Tasty.it "works" $ do
-      let secret = Secret.MkSecret . ByteArray.convert $ Ascii.pack "0123456789"
-      Secret.render secret @?= Ascii.pack "whsec_MDEyMzQ1Njc4OQ=="
diff --git a/source/libraries/test/Signet/Unstable/Type/SignatureTest.hs b/source/libraries/test/Signet/Unstable/Type/SignatureTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Type/SignatureTest.hs
+++ /dev/null
@@ -1,53 +0,0 @@
-module Signet.Unstable.Type.SignatureTest where
-
-import qualified Crypto.Error as Error
-import qualified Crypto.Hash as Hash
-import qualified Crypto.PubKey.Ed25519 as Ed25519
-import qualified Data.ByteString as ByteString
-import qualified Data.ByteString.Char8 as Ascii
-import qualified Signet.Unstable.Exception.InvalidAsymmetricSignature as InvalidAsymmetricSignature
-import qualified Signet.Unstable.Exception.InvalidSignature as InvalidSignature
-import qualified Signet.Unstable.Exception.InvalidSymmetricSignature as InvalidSymmetricSignature
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-import qualified Signet.Unstable.Type.AsymmetricSignature as AsymmetricSignature
-import qualified Signet.Unstable.Type.Signature as Signature
-import qualified Signet.Unstable.Type.SymmetricSignature as SymmetricSignature
-import qualified Signet.Unstable.Type.UnknownSignature as UnknownSignature
-import Test.Tasty.HUnit ((@?=))
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Type.Signature" $ do
-  Tasty.describe "parse" $ do
-    Tasty.it "fails with invalid asymmetric signature" $ do
-      let byteString = Ascii.pack "invalid"
-      let result = Signature.parse $ Ascii.pack "v1a," <> byteString
-      result @?= Left (InvalidSignature.InvalidAsymmetricSignature $ InvalidAsymmetricSignature.MkInvalidAsymmetricSignature byteString)
-
-    Tasty.it "fails with invalid symmetric signature" $ do
-      let byteString = Ascii.pack "invalid"
-      let result = Signature.parse $ Ascii.pack "v1," <> byteString
-      result @?= Left (InvalidSignature.InvalidSymmetricSignature $ InvalidSymmetricSignature.MkInvalidSymmetricSignature byteString)
-
-    Tasty.it "returns unknown signature with unrecognized prefix" $ do
-      let byteString = Ascii.pack "unknown"
-      let result = Signature.parse byteString
-      result @?= Right (Left $ UnknownSignature.MkUnknownSignature byteString)
-
-    Tasty.it "succeeds with valid asymmetric signature" $ do
-      let byteString = Ascii.pack "v1a,QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVotMDEyMzQ1Njc4OS1hYmNkZWZnaGlqa2xtbm9wcXJzdHV2cXh5eg=="
-      signature <- fmap (Signature.Asymmetric . AsymmetricSignature.MkAsymmetricSignature) . Error.throwCryptoErrorIO . Ed25519.signature $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789-abcdefghijklmnopqrstuvqxyz"
-      Signature.parse byteString @?= Right (Right signature)
-
-    Tasty.it "succeeds with valid symmetric signature" $ do
-      let byteString = Ascii.pack "v1,47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
-      let signature = Signature.Symmetric . SymmetricSignature.MkSymmetricSignature $ Hash.hash ByteString.empty
-      Signature.parse byteString @?= Right (Right signature)
-
-  Tasty.describe "render" $ do
-    Tasty.it "renders asymmetric signature correctly" $ do
-      signature <- fmap (Signature.Asymmetric . AsymmetricSignature.MkAsymmetricSignature) . Error.throwCryptoErrorIO . Ed25519.signature $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789-abcdefghijklmnopqrstuvqxyz"
-      Signature.render signature @?= Ascii.pack "v1a,QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVotMDEyMzQ1Njc4OS1hYmNkZWZnaGlqa2xtbm9wcXJzdHV2cXh5eg=="
-
-    Tasty.it "renders symmetric signature correctly" $ do
-      let signature = Signature.Symmetric . SymmetricSignature.MkSymmetricSignature $ Hash.hash ByteString.empty
-      Signature.render signature @?= Ascii.pack "v1,47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
diff --git a/source/libraries/test/Signet/Unstable/Type/SignaturesTest.hs b/source/libraries/test/Signet/Unstable/Type/SignaturesTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Type/SignaturesTest.hs
+++ /dev/null
@@ -1,72 +0,0 @@
-module Signet.Unstable.Type.SignaturesTest where
-
-import qualified Crypto.Error as Error
-import qualified Crypto.Hash as Hash
-import qualified Crypto.PubKey.Ed25519 as Ed25519
-import qualified Data.ByteString as ByteString
-import qualified Data.ByteString.Char8 as Ascii
-import qualified Signet.Unstable.Exception.InvalidAsymmetricSignature as InvalidAsymmetricSignature
-import qualified Signet.Unstable.Exception.InvalidSignature as InvalidSignature
-import qualified Signet.Unstable.Exception.InvalidSymmetricSignature as InvalidSymmetricSignature
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-import qualified Signet.Unstable.Type.AsymmetricSignature as AsymmetricSignature
-import qualified Signet.Unstable.Type.Signature as Signature
-import qualified Signet.Unstable.Type.Signatures as Signatures
-import qualified Signet.Unstable.Type.SymmetricSignature as SymmetricSignature
-import qualified Signet.Unstable.Type.UnknownSignature as UnknownSignature
-import Test.Tasty.HUnit ((@?=))
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Type.Signatures" $ do
-  Tasty.describe "parse" $ do
-    Tasty.it "succeeds with no signatures" $ do
-      Signatures.parse ByteString.empty @?= Right ([], Signatures.MkSignatures [])
-
-    Tasty.it "succeeds with one signature" $ do
-      let signature = Signature.Symmetric . SymmetricSignature.MkSymmetricSignature $ Hash.hash ByteString.empty
-      let byteString = Ascii.pack "v1,47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
-      Signatures.parse byteString @?= Right ([], Signatures.MkSignatures [signature])
-
-    Tasty.it "succeeds with many signatures" $ do
-      let symmetricSignature = SymmetricSignature.MkSymmetricSignature $ Hash.hash ByteString.empty
-      asymmetricSignature <- fmap AsymmetricSignature.MkAsymmetricSignature . Error.throwCryptoErrorIO . Ed25519.signature $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789-abcdefghijklmnopqrstuvqxyz"
-      let byteString = Ascii.pack "v1,47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU= v1a,QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVotMDEyMzQ1Njc4OS1hYmNkZWZnaGlqa2xtbm9wcXJzdHV2cXh5eg=="
-      let signatures =
-            Signatures.MkSignatures
-              [ Signature.Symmetric symmetricSignature,
-                Signature.Asymmetric asymmetricSignature
-              ]
-      Signatures.parse byteString @?= Right ([], signatures)
-
-    Tasty.it "fails with an invalid symmetric signature" $ do
-      let x = Ascii.pack "invalid"
-      let byteString = Ascii.pack "v1," <> x
-      Signatures.parse byteString @?= Left (InvalidSignature.InvalidSymmetricSignature $ InvalidSymmetricSignature.MkInvalidSymmetricSignature x)
-
-    Tasty.it "fails with an invalid asymmetric signature" $ do
-      let x = Ascii.pack "invalid"
-      let byteString = Ascii.pack "v1a," <> x
-      Signatures.parse byteString @?= Left (InvalidSignature.InvalidAsymmetricSignature $ InvalidAsymmetricSignature.MkInvalidAsymmetricSignature x)
-
-    Tasty.it "succeeds with an unknown signature" $ do
-      let byteString = Ascii.pack "unknown"
-      Signatures.parse byteString @?= Right ([UnknownSignature.MkUnknownSignature byteString], Signatures.MkSignatures [])
-
-  Tasty.describe "render" $ do
-    Tasty.it "renders no signatures" $ do
-      let signatures = Signatures.MkSignatures []
-      Signatures.render signatures @?= ByteString.empty
-
-    Tasty.it "renders one signature" $ do
-      let signatures = Signatures.MkSignatures . pure . Signature.Symmetric . SymmetricSignature.MkSymmetricSignature $ Hash.hash ByteString.empty
-      Signatures.render signatures @?= Ascii.pack "v1,47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
-
-    Tasty.it "renders many signatures" $ do
-      let symmetricSignature = SymmetricSignature.MkSymmetricSignature $ Hash.hash ByteString.empty
-      asymmetricSignature <- fmap AsymmetricSignature.MkAsymmetricSignature . Error.throwCryptoErrorIO . Ed25519.signature $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789-abcdefghijklmnopqrstuvqxyz"
-      let signatures =
-            Signatures.MkSignatures
-              [ Signature.Symmetric symmetricSignature,
-                Signature.Asymmetric asymmetricSignature
-              ]
-      Signatures.render signatures @?= Ascii.pack "v1,47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU= v1a,QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVotMDEyMzQ1Njc4OS1hYmNkZWZnaGlqa2xtbm9wcXJzdHV2cXh5eg=="
diff --git a/source/libraries/test/Signet/Unstable/Type/SignerTest.hs b/source/libraries/test/Signet/Unstable/Type/SignerTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Type/SignerTest.hs
+++ /dev/null
@@ -1,76 +0,0 @@
-module Signet.Unstable.Type.SignerTest where
-
-import qualified Data.ByteString.Char8 as Ascii
-import qualified Signet.Unstable.Exception.InvalidSigner as InvalidSigner
-import qualified Signet.Unstable.Extra.Either as Either
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-import qualified Signet.Unstable.Type.AsymmetricSignature as AsymmetricSignature
-import qualified Signet.Unstable.Type.Message as Message
-import qualified Signet.Unstable.Type.Secret as Secret
-import qualified Signet.Unstable.Type.SecretKey as SecretKey
-import qualified Signet.Unstable.Type.Signature as Signature
-import qualified Signet.Unstable.Type.Signer as Signer
-import qualified Signet.Unstable.Type.SymmetricSignature as SymmetricSignature
-import Test.Tasty.HUnit ((@?=))
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Type.Signer" $ do
-  Tasty.describe "parse" $ do
-    Tasty.it "succeeds with a valid secret key" $ do
-      let byteString = Ascii.pack "whsk_QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU="
-      let actual = Signer.parse byteString
-      secretKey <- Either.throw $ SecretKey.parse byteString
-      actual @?= Right (Signer.Asymmetric secretKey)
-
-    Tasty.it "succeeds with a valid secret" $ do
-      let byteString = Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
-      let actual = Signer.parse byteString
-      secret <- Either.throw $ Secret.parse byteString
-      actual @?= Right (Signer.Symmetric secret)
-
-    Tasty.it "fails with invalid input" $ do
-      let byteString = Ascii.pack "invalid"
-      let actual = Signer.parse byteString
-      actual @?= Left (InvalidSigner.MkInvalidSigner byteString)
-
-  Tasty.describe "render" $ do
-    Tasty.it "works with a secret key" $ do
-      let byteString = Ascii.pack "whsk_QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU="
-      secretKey <- Either.throw . SecretKey.parse $ byteString
-      Signer.render (Signer.Asymmetric secretKey) @?= byteString
-
-    Tasty.it "works with a secret" $ do
-      let byteString = Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
-      secret <- Either.throw . Secret.parse $ byteString
-      Signer.render (Signer.Symmetric secret) @?= byteString
-
-  Tasty.describe "sign" $ do
-    Tasty.it "works with asymmetric" $ do
-      signer <- Either.throw . Signer.parse $ Ascii.pack "whsk_QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU="
-      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
-      let actual = Signer.sign signer message
-      Right expected <- Either.throw . Signature.parse $ Ascii.pack "v1a,CV1O+PvrwXM42OMUX+tmm6bA3cS0tgLp0qo3YKuu0MGmBrsUhA0MHXF11HsEUJtPfTKs80WE7WUKVt9TueLDCQ=="
-      actual @?= expected
-
-    Tasty.it "works with symmetric" $ do
-      signer <- Either.throw . Signer.parse $ Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
-      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
-      let actual = Signer.sign signer message
-      Right expected <- Either.throw . Signature.parse $ Ascii.pack "v1,IywpE5NXy+JdAScgR7j5Pt59GjmazD7iJuVsQoRZFyw="
-      actual @?= expected
-
-  Tasty.describe "asymmetric" $ do
-    Tasty.it "creates correct asymmetric signature" $ do
-      secretKey <- Either.throw . SecretKey.parse $ Ascii.pack "whsk_QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU="
-      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
-      let actual = Signer.asymmetric secretKey message
-      expected <- Either.throw . AsymmetricSignature.parse $ Ascii.pack "CV1O+PvrwXM42OMUX+tmm6bA3cS0tgLp0qo3YKuu0MGmBrsUhA0MHXF11HsEUJtPfTKs80WE7WUKVt9TueLDCQ=="
-      actual @?= expected
-
-  Tasty.describe "symmetric" $ do
-    Tasty.it "creates correct symmetric signature" $ do
-      secret <- Either.throw . Secret.parse $ Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
-      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
-      let actual = Signer.symmetric secret message
-      expected <- Either.throw . SymmetricSignature.parse $ Ascii.pack "IywpE5NXy+JdAScgR7j5Pt59GjmazD7iJuVsQoRZFyw="
-      actual @?= expected
diff --git a/source/libraries/test/Signet/Unstable/Type/SymmetricSignatureTest.hs b/source/libraries/test/Signet/Unstable/Type/SymmetricSignatureTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Type/SymmetricSignatureTest.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-module Signet.Unstable.Type.SymmetricSignatureTest where
-
-import qualified Crypto.Hash as Hash
-import qualified Data.ByteString as ByteString
-import qualified Data.ByteString.Char8 as Ascii
-import qualified Signet.Unstable.Exception.InvalidSymmetricSignature as InvalidSymmetricSignature
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-import qualified Signet.Unstable.Type.SymmetricSignature as SymmetricSignature
-import Test.Tasty.HUnit ((@?=))
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Type.SymmetricSignature" $ do
-  Tasty.describe "parse" $ do
-    Tasty.it "fails with invalid symmetric signature" $ do
-      let byteString = Ascii.pack "invalid"
-      let result = SymmetricSignature.parse byteString
-      result @?= Left (InvalidSymmetricSignature.MkInvalidSymmetricSignature byteString)
-
-    Tasty.it "succeeds with valid symmetric signature" $ do
-      let byteString = Ascii.pack "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
-      let signature = SymmetricSignature.MkSymmetricSignature $ Hash.hash ByteString.empty
-      SymmetricSignature.parse byteString @?= Right signature
-
-  Tasty.describe "render" $ do
-    Tasty.it "renders symmetric signature correctly" $ do
-      let signature = SymmetricSignature.MkSymmetricSignature $ Hash.hash ByteString.empty
-      SymmetricSignature.render signature @?= Ascii.pack "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
diff --git a/source/libraries/test/Signet/Unstable/Type/TimestampTest.hs b/source/libraries/test/Signet/Unstable/Type/TimestampTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Type/TimestampTest.hs
+++ /dev/null
@@ -1,33 +0,0 @@
-module Signet.Unstable.Type.TimestampTest where
-
-import qualified Data.ByteString.Char8 as Ascii
-import qualified Data.Time as Time
-import qualified Signet.Unstable.Exception.InvalidTimestamp as InvalidTimestamp
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-import qualified Signet.Unstable.Type.Timestamp as Timestamp
-import Test.Tasty.HUnit ((@?=))
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Type.Timestamp" $ do
-  Tasty.describe "parse" $ do
-    Tasty.it "fails with invalid timestamp format" $ do
-      let byteString = Ascii.pack "invalid-timestamp"
-      let result = Timestamp.parse byteString
-      result @?= Left (InvalidTimestamp.MkInvalidTimestamp byteString)
-
-    Tasty.it "succeeds with valid timestamp format" $ do
-      let byteString = Ascii.pack "1617235200"
-      let result = Timestamp.parse byteString
-      case result of
-        Right timestamp -> do
-          let utcTime = Timestamp.unwrap timestamp
-          let expectedTime = Time.UTCTime (Time.fromGregorian 2021 4 1) 0
-          utcTime @?= expectedTime
-        Left _ -> fail "Expected Right but got Left"
-
-  Tasty.describe "render" $ do
-    Tasty.it "returns the correct ByteString representation" $ do
-      let utcTime = Time.UTCTime (Time.fromGregorian 2021 4 1) 0
-      let timestamp = Timestamp.MkTimestamp utcTime
-      let expected = Ascii.pack "1617235200"
-      Timestamp.render timestamp @?= expected
diff --git a/source/libraries/test/Signet/Unstable/Type/ToleranceTest.hs b/source/libraries/test/Signet/Unstable/Type/ToleranceTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Type/ToleranceTest.hs
+++ /dev/null
@@ -1,34 +0,0 @@
-module Signet.Unstable.Type.ToleranceTest where
-
-import qualified Data.Time as Time
-import qualified Signet.Unstable.Exception.ToleranceException as ToleranceException
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-import qualified Signet.Unstable.Type.Timestamp as Timestamp
-import qualified Signet.Unstable.Type.Tolerance as Tolerance
-import Test.Tasty.HUnit ((@?=))
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Type.Tolerance" $ do
-  Tasty.describe "check" $ do
-    Tasty.it "succeeds when timestamp is within tolerance" $ do
-      let tolerance = Tolerance.MkTolerance 1
-      let utcTime = Time.UTCTime (Time.fromGregorian 2001 1 1) 60
-      let timestamp = Timestamp.MkTimestamp utcTime
-      let result = Tolerance.check tolerance utcTime timestamp
-      result @?= Right ()
-
-    Tasty.it "fails when timestamp is too old" $ do
-      let tolerance = Tolerance.MkTolerance 1
-      let utcTime = Time.UTCTime (Time.fromGregorian 2001 1 1) 60
-      let timestamp = Timestamp.MkTimestamp utcTime
-      let now = Time.addUTCTime 2 utcTime
-      let result = Tolerance.check tolerance now timestamp
-      result @?= Left (ToleranceException.MkToleranceException timestamp)
-
-    Tasty.it "fails when timestamp is in the future" $ do
-      let tolerance = Tolerance.MkTolerance 1
-      let utcTime = Time.UTCTime (Time.fromGregorian 2001 1 1) 60
-      let timestamp = Timestamp.MkTimestamp utcTime
-      let now = Time.addUTCTime (-2) utcTime
-      let result = Tolerance.check tolerance now timestamp
-      result @?= Left (ToleranceException.MkToleranceException timestamp)
diff --git a/source/libraries/test/Signet/Unstable/Type/UnknownSignatureTest.hs b/source/libraries/test/Signet/Unstable/Type/UnknownSignatureTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Type/UnknownSignatureTest.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Signet.Unstable.Type.UnknownSignatureTest where
-
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Type.UnknownSignature" $ do
-  pure ()
diff --git a/source/libraries/test/Signet/Unstable/Type/VerifierTest.hs b/source/libraries/test/Signet/Unstable/Type/VerifierTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/Unstable/Type/VerifierTest.hs
+++ /dev/null
@@ -1,110 +0,0 @@
-module Signet.Unstable.Type.VerifierTest where
-
-import qualified Data.ByteString.Char8 as Ascii
-import qualified Signet.Unstable.Exception.InvalidVerifier as InvalidVerifier
-import qualified Signet.Unstable.Exception.VerificationException as VerificationException
-import qualified Signet.Unstable.Extra.Either as Either
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-import qualified Signet.Unstable.Type.AsymmetricSignature as AsymmetricSignature
-import qualified Signet.Unstable.Type.Message as Message
-import qualified Signet.Unstable.Type.PublicKey as PublicKey
-import qualified Signet.Unstable.Type.Secret as Secret
-import qualified Signet.Unstable.Type.Signature as Signature
-import qualified Signet.Unstable.Type.Signatures as Signatures
-import qualified Signet.Unstable.Type.SymmetricSignature as SymmetricSignature
-import qualified Signet.Unstable.Type.Verifier as Verifier
-import Test.Tasty.HUnit ((@?=))
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable.Type.Verifier" $ do
-  Tasty.describe "parse" $ do
-    Tasty.it "succeeds with a valid public key" $ do
-      let byteString = Ascii.pack "whpk_wuzPrKxPfWpJSsXgyg/MEoMGvjs5SjDO4ad6X4ZYqqg="
-      let actual = Verifier.parse byteString
-      publicKey <- Either.throw $ PublicKey.parse byteString
-      actual @?= Right (Verifier.Asymmetric publicKey)
-
-    Tasty.it "succeeds with a valid secret" $ do
-      let byteString = Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
-      let actual = Verifier.parse byteString
-      secret <- Either.throw $ Secret.parse byteString
-      actual @?= Right (Verifier.Symmetric secret)
-
-    Tasty.it "fails with invalid input" $ do
-      let byteString = Ascii.pack "invalid"
-      let actual = Verifier.parse byteString
-      actual @?= Left (InvalidVerifier.MkInvalidVerifier byteString)
-
-  Tasty.describe "render" $ do
-    Tasty.it "works with a public key" $ do
-      let byteString = Ascii.pack "whpk_wuzPrKxPfWpJSsXgyg/MEoMGvjs5SjDO4ad6X4ZYqqg="
-      publicKey <- Either.throw . PublicKey.parse $ byteString
-      Verifier.render (Verifier.Asymmetric publicKey) @?= byteString
-
-    Tasty.it "works with a secret" $ do
-      let byteString = Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
-      secret <- Either.throw . Secret.parse $ byteString
-      Verifier.render (Verifier.Symmetric secret) @?= byteString
-
-  Tasty.describe "verify" $ do
-    Tasty.it "succeeds with a valid asymmetric signature" $ do
-      verifier <- Either.throw . Verifier.parse $ Ascii.pack "whpk_wuzPrKxPfWpJSsXgyg/MEoMGvjs5SjDO4ad6X4ZYqqg="
-      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
-      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1a,CV1O+PvrwXM42OMUX+tmm6bA3cS0tgLp0qo3YKuu0MGmBrsUhA0MHXF11HsEUJtPfTKs80WE7WUKVt9TueLDCQ=="
-      let actual = Verifier.verify verifier message signatures
-      Right signature <- Either.throw . Signature.parse $ Ascii.pack "v1a,CV1O+PvrwXM42OMUX+tmm6bA3cS0tgLp0qo3YKuu0MGmBrsUhA0MHXF11HsEUJtPfTKs80WE7WUKVt9TueLDCQ=="
-      actual @?= Right signature
-
-    Tasty.it "fails with an invalid asymmetric signature" $ do
-      verifier <- Either.throw . Verifier.parse $ Ascii.pack "whpk_wuzPrKxPfWpJSsXgyg/MEoMGvjs5SjDO4ad6X4ZYqqg="
-      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
-      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1a,00000000000000000000000000000000000000000000000000000000000000000000000000000000000000=="
-      let actual = Verifier.verify verifier message signatures
-      actual @?= Left (VerificationException.MkVerificationException $ Message.id_ message)
-
-    Tasty.it "succeeds with a valid symmetric signature" $ do
-      verifier <- Either.throw . Verifier.parse $ Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
-      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
-      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1,IywpE5NXy+JdAScgR7j5Pt59GjmazD7iJuVsQoRZFyw="
-      let actual = Verifier.verify verifier message signatures
-      Right signature <- Either.throw . Signature.parse $ Ascii.pack "v1,IywpE5NXy+JdAScgR7j5Pt59GjmazD7iJuVsQoRZFyw="
-      actual @?= Right signature
-
-    Tasty.it "fails with an invalid symmetric signature" $ do
-      verifier <- Either.throw . Verifier.parse $ Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
-      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
-      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1,0000000000000000000000000000000000000000000="
-      let actual = Verifier.verify verifier message signatures
-      actual @?= Left (VerificationException.MkVerificationException $ Message.id_ message)
-
-  Tasty.describe "asymmetric" $ do
-    Tasty.it "succeeds with a valid signature" $ do
-      publicKey <- Either.throw . PublicKey.parse $ Ascii.pack "whpk_wuzPrKxPfWpJSsXgyg/MEoMGvjs5SjDO4ad6X4ZYqqg="
-      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
-      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1a,CV1O+PvrwXM42OMUX+tmm6bA3cS0tgLp0qo3YKuu0MGmBrsUhA0MHXF11HsEUJtPfTKs80WE7WUKVt9TueLDCQ=="
-      let actual = Verifier.asymmetric publicKey message signatures
-      asymmetricSignature <- Either.throw . AsymmetricSignature.parse $ Ascii.pack "CV1O+PvrwXM42OMUX+tmm6bA3cS0tgLp0qo3YKuu0MGmBrsUhA0MHXF11HsEUJtPfTKs80WE7WUKVt9TueLDCQ=="
-      actual @?= Right asymmetricSignature
-
-    Tasty.it "fails with an invalid signature" $ do
-      publicKey <- Either.throw . PublicKey.parse $ Ascii.pack "whpk_wuzPrKxPfWpJSsXgyg/MEoMGvjs5SjDO4ad6X4ZYqqg="
-      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
-      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1a,00000000000000000000000000000000000000000000000000000000000000000000000000000000000000=="
-      let actual = Verifier.asymmetric publicKey message signatures
-      actual @?= Left (VerificationException.MkVerificationException $ Message.id_ message)
-
-  Tasty.describe "symmetric" $ do
-    Tasty.it "succeeds with a valid signature" $ do
-      secret <- Either.throw . Secret.parse $ Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
-      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
-      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1,IywpE5NXy+JdAScgR7j5Pt59GjmazD7iJuVsQoRZFyw="
-      let actual = Verifier.symmetric secret message signatures
-      symmetricSignature <- Either.throw . SymmetricSignature.parse $ Ascii.pack "IywpE5NXy+JdAScgR7j5Pt59GjmazD7iJuVsQoRZFyw="
-      actual @?= Right symmetricSignature
-
-    Tasty.it "fails with an invalid signature" $ do
-      secret <- Either.throw . Secret.parse $ Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
-      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
-      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1,0000000000000000000000000000000000000000000="
-      let actual = Verifier.symmetric secret message signatures
-      actual @?= Left (VerificationException.MkVerificationException $ Message.id_ message)
diff --git a/source/libraries/test/Signet/UnstableTest.hs b/source/libraries/test/Signet/UnstableTest.hs
deleted file mode 100644
--- a/source/libraries/test/Signet/UnstableTest.hs
+++ /dev/null
@@ -1,57 +0,0 @@
-module Signet.UnstableTest where
-
-import qualified Data.ByteString.Char8 as Ascii
-import qualified Data.Time as Time
-import qualified Signet.Unstable as Signet
-import qualified Signet.Unstable.Exception.SignetException as SignetException
-import qualified Signet.Unstable.Exception.ToleranceException as ToleranceException
-import qualified Signet.Unstable.Exception.VerificationException as VerificationException
-import qualified Signet.Unstable.Extra.Either as Either
-import qualified Signet.Unstable.Extra.Tasty as Tasty
-import qualified Signet.Unstable.Type.Message as Message
-import qualified Signet.Unstable.Type.Signature as Signature
-import qualified Signet.Unstable.Type.Signatures as Signatures
-import qualified Signet.Unstable.Type.Tolerance as Tolerance
-import qualified Signet.Unstable.Type.Verifier as Verifier
-import Test.Tasty.HUnit ((@?=))
-
-spec :: Tasty.Spec
-spec = Tasty.describe "Signet.Unstable" $ do
-  Tasty.describe "verifyWebhookText" $ do
-    pure ()
-
-  Tasty.describe "verifyWebhookByteString" $ do
-    pure ()
-
-  Tasty.describe "verifyWebhook" $ do
-    pure ()
-
-  Tasty.describe "verifyWebhookWith" $ do
-    Tasty.it "succeeds with a valid symmetric signature" $ do
-      let tolerance = Tolerance.typical
-      let now = Time.UTCTime (Time.fromGregorian 1970 1 1) 0
-      verifier <- Either.throw . Verifier.parse $ Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
-      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
-      let byteString = Ascii.pack "v1,IywpE5NXy+JdAScgR7j5Pt59GjmazD7iJuVsQoRZFyw="
-      (_, signatures) <- Either.throw $ Signatures.parse byteString
-      let result = Signet.verifyWebhookWith tolerance now verifier message signatures
-      Right signature <- Either.throw $ Signature.parse byteString
-      result @?= Right signature
-
-    Tasty.it "fails with an invalid timestamp" $ do
-      let tolerance = Tolerance.typical
-      let now = Time.UTCTime (Time.fromGregorian 1970 1 1) 0
-      verifier <- Either.throw . Verifier.parse $ Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
-      message <- Either.throw . Message.parse $ Ascii.pack "i.301.Hello, world!"
-      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1,IywpE5NXy+JdAScgR7j5Pt59GjmazD7iJuVsQoRZFyw="
-      let result = Signet.verifyWebhookWith tolerance now verifier message signatures
-      result @?= Left (SignetException.ToleranceException . ToleranceException.MkToleranceException $ Message.timestamp message)
-
-    Tasty.it "fails with an invalid symmetric signature" $ do
-      let tolerance = Tolerance.typical
-      let now = Time.UTCTime (Time.fromGregorian 1970 1 1) 0
-      verifier <- Either.throw . Verifier.parse $ Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
-      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
-      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1,0000000000000000000000000000000000000000000="
-      let result = Signet.verifyWebhookWith tolerance now verifier message signatures
-      result @?= Left (SignetException.VerificationException . VerificationException.MkVerificationException $ Message.id_ message)
diff --git a/source/libraries/test/SignetTest.hs b/source/libraries/test/SignetTest.hs
deleted file mode 100644
--- a/source/libraries/test/SignetTest.hs
+++ /dev/null
@@ -1,73 +0,0 @@
-module SignetTest where
-
-import qualified Control.Monad.Trans.Writer as Writer
-import qualified Signet.Unstable.Exception.InvalidAsymmetricSignatureTest
-import qualified Signet.Unstable.Exception.InvalidIdTest
-import qualified Signet.Unstable.Exception.InvalidMessageTest
-import qualified Signet.Unstable.Exception.InvalidPublicKeyTest
-import qualified Signet.Unstable.Exception.InvalidSecretKeyTest
-import qualified Signet.Unstable.Exception.InvalidSecretTest
-import qualified Signet.Unstable.Exception.InvalidSignatureTest
-import qualified Signet.Unstable.Exception.InvalidSignerTest
-import qualified Signet.Unstable.Exception.InvalidSymmetricSignatureTest
-import qualified Signet.Unstable.Exception.InvalidTimestampTest
-import qualified Signet.Unstable.Exception.InvalidVerifierTest
-import qualified Signet.Unstable.Exception.SignetExceptionTest
-import qualified Signet.Unstable.Exception.ToleranceExceptionTest
-import qualified Signet.Unstable.Exception.VerificationExceptionTest
-import qualified Signet.Unstable.Extra.EitherTest
-import qualified Signet.Unstable.Extra.HttpTest
-import qualified Signet.Unstable.Extra.MaybeTest
-import qualified Signet.Unstable.Type.AsymmetricSignatureTest
-import qualified Signet.Unstable.Type.IdTest
-import qualified Signet.Unstable.Type.MessageTest
-import qualified Signet.Unstable.Type.PayloadTest
-import qualified Signet.Unstable.Type.PublicKeyTest
-import qualified Signet.Unstable.Type.SecretKeyTest
-import qualified Signet.Unstable.Type.SecretTest
-import qualified Signet.Unstable.Type.SignatureTest
-import qualified Signet.Unstable.Type.SignaturesTest
-import qualified Signet.Unstable.Type.SignerTest
-import qualified Signet.Unstable.Type.SymmetricSignatureTest
-import qualified Signet.Unstable.Type.TimestampTest
-import qualified Signet.Unstable.Type.ToleranceTest
-import qualified Signet.Unstable.Type.UnknownSignatureTest
-import qualified Signet.Unstable.Type.VerifierTest
-import qualified Signet.UnstableTest
-import qualified Test.Tasty as Tasty
-
-testTree :: Tasty.TestTree
-testTree = Tasty.testGroup "signet" . Writer.execWriter $ do
-  Signet.Unstable.Exception.InvalidAsymmetricSignatureTest.spec
-  Signet.Unstable.Exception.InvalidIdTest.spec
-  Signet.Unstable.Exception.InvalidMessageTest.spec
-  Signet.Unstable.Exception.InvalidPublicKeyTest.spec
-  Signet.Unstable.Exception.InvalidSecretKeyTest.spec
-  Signet.Unstable.Exception.InvalidSecretTest.spec
-  Signet.Unstable.Exception.InvalidSignatureTest.spec
-  Signet.Unstable.Exception.InvalidSignerTest.spec
-  Signet.Unstable.Exception.InvalidSymmetricSignatureTest.spec
-  Signet.Unstable.Exception.InvalidTimestampTest.spec
-  Signet.Unstable.Exception.InvalidVerifierTest.spec
-  Signet.Unstable.Exception.SignetExceptionTest.spec
-  Signet.Unstable.Exception.ToleranceExceptionTest.spec
-  Signet.Unstable.Exception.VerificationExceptionTest.spec
-  Signet.Unstable.Extra.EitherTest.spec
-  Signet.Unstable.Extra.HttpTest.spec
-  Signet.Unstable.Extra.MaybeTest.spec
-  Signet.Unstable.Type.AsymmetricSignatureTest.spec
-  Signet.Unstable.Type.IdTest.spec
-  Signet.Unstable.Type.MessageTest.spec
-  Signet.Unstable.Type.PayloadTest.spec
-  Signet.Unstable.Type.PublicKeyTest.spec
-  Signet.Unstable.Type.SecretKeyTest.spec
-  Signet.Unstable.Type.SecretTest.spec
-  Signet.Unstable.Type.SignaturesTest.spec
-  Signet.Unstable.Type.SignatureTest.spec
-  Signet.Unstable.Type.SignerTest.spec
-  Signet.Unstable.Type.SymmetricSignatureTest.spec
-  Signet.Unstable.Type.TimestampTest.spec
-  Signet.Unstable.Type.ToleranceTest.spec
-  Signet.Unstable.Type.UnknownSignatureTest.spec
-  Signet.Unstable.Type.VerifierTest.spec
-  Signet.UnstableTest.spec
diff --git a/source/libraries/unstable/Signet/Unstable.hs b/source/libraries/unstable/Signet/Unstable.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable.hs
+++ /dev/null
@@ -1,196 +0,0 @@
-{-# LANGUAGE PatternSynonyms #-}
-
-module Signet.Unstable where
-
-import qualified Control.Monad.Catch as Exception
-import qualified Control.Monad.IO.Class as MonadIO
-import qualified Data.Bifunctor as Bifunctor
-import qualified Data.ByteString as ByteString
-import qualified Data.Text as Text
-import qualified Data.Text.Encoding as Text
-import qualified Data.Time as Time
-import qualified Signet.Unstable.Exception.InvalidId as InvalidId
-import qualified Signet.Unstable.Exception.InvalidMessage as InvalidMessage
-import qualified Signet.Unstable.Exception.InvalidSignature as InvalidSignature
-import qualified Signet.Unstable.Exception.InvalidSigner as InvalidSigner
-import qualified Signet.Unstable.Exception.InvalidTimestamp as InvalidTimestamp
-import qualified Signet.Unstable.Exception.InvalidVerifier as InvalidVerifier
-import qualified Signet.Unstable.Exception.SignetException as SignetException
-import qualified Signet.Unstable.Extra.Either as Either
-import qualified Signet.Unstable.Type.AsymmetricSignature as AsymmetricSignature
-import qualified Signet.Unstable.Type.Id as Id
-import qualified Signet.Unstable.Type.Message as Message
-import qualified Signet.Unstable.Type.Payload as Payload
-import qualified Signet.Unstable.Type.PublicKey as PublicKey
-import qualified Signet.Unstable.Type.Secret as Secret
-import qualified Signet.Unstable.Type.SecretKey as SecretKey
-import qualified Signet.Unstable.Type.Signature as Signature
-import qualified Signet.Unstable.Type.Signatures as Signatures
-import qualified Signet.Unstable.Type.Signer as Signer
-import qualified Signet.Unstable.Type.SymmetricSignature as SymmetricSignature
-import qualified Signet.Unstable.Type.Timestamp as Timestamp
-import qualified Signet.Unstable.Type.Tolerance as Tolerance
-import qualified Signet.Unstable.Type.UnknownSignature as UnknownSignature
-import qualified Signet.Unstable.Type.Verifier as Verifier
-
--- | Verifies a webhook with 'Text.Text' values. This is a wrapper around
--- 'verifyWebhookByteString' that assumes all values are encoded as UTF-8.
-verifyWebhookText ::
-  (MonadIO.MonadIO m, Exception.MonadThrow m) =>
-  -- | A 'Verifier.Verifier' for the webhook. Typically this will be
-  -- @"whsec_..."@ or @"whpk_..."@.
-  Text.Text ->
-  -- | The webhook's unique 'Id.Id'. Typically this will come from the
-  -- 'Signet.Unstable.Extra.Http.hWebhookId' header and look like @"msg_..."@.
-  Text.Text ->
-  -- | The webhook's 'Timestamp.Timestamp'. This is an integer number of
-  -- seconds since the Unix epoch. Typically this will come from the
-  -- 'Signet.Unstable.Extra.Http.hWebhookTimestamp' header. For example
-  -- @"981173106"@ for @2001-02-03T04:05:06Z@.
-  Text.Text ->
-  -- | The webhook's raw 'Payload.Payload'. Typically this will be a JSON
-  -- object like @{"event_type":"ping",...}@.
-  Text.Text ->
-  -- | The webhook's 'Signatures.Signatures'. Typically this will come from the
-  -- 'Signet.Unstable.Extra.Http.hWebhookSignature' header and look like
-  -- @"v1_..."@ or @"v1a_..."@.
-  Text.Text ->
-  m Signature.Signature
-verifyWebhookText v i t p s =
-  verifyWebhookByteString
-    (Text.encodeUtf8 v)
-    (Text.encodeUtf8 i)
-    (Text.encodeUtf8 t)
-    (Text.encodeUtf8 p)
-    (Text.encodeUtf8 s)
-
--- | Verifies a webhook with 'ByteString.ByteString' values. This is a
--- wrapper around 'verifyWebhook'. See 'verifyWebhookText' for a description of
--- the arguments.
-verifyWebhookByteString ::
-  (MonadIO.MonadIO m, Exception.MonadThrow m) =>
-  -- | 'Verifier.Verifier'
-  ByteString.ByteString ->
-  -- | 'Id.Id'
-  ByteString.ByteString ->
-  -- | 'Timestamp.Timestamp'
-  ByteString.ByteString ->
-  -- | 'Payload.Payload'
-  ByteString.ByteString ->
-  -- | 'Signatures.Signatures'
-  ByteString.ByteString ->
-  m Signature.Signature
-verifyWebhookByteString v i t p s = do
-  verifier <- Either.throw $ parseVerifier v
-  id_ <- Either.throw $ parseId i
-  timestamp <- Either.throw $ parseTimestamp t
-  let payload = Payload.MkPayload p
-  let message =
-        Message.MkMessage
-          { Message.id_ = id_,
-            Message.timestamp = timestamp,
-            Message.payload = payload
-          }
-  (_, signatures) <- Either.throw $ parseSignatures s
-  verifyWebhook verifier message signatures
-
--- | Verifies a webhook. This is a wrapper around 'verifyWebhookWith' that uses
--- 'typicalTolerance' and the current time.
---
--- Throws an exception if the webhook is invalid.
-verifyWebhook ::
-  (MonadIO.MonadIO m, Exception.MonadThrow m) =>
-  Verifier.Verifier ->
-  Message.Message ->
-  Signatures.Signatures ->
-  m Signature.Signature
-verifyWebhook verifier message signatures = do
-  now <- MonadIO.liftIO Time.getCurrentTime
-  Either.throw $ verifyWebhookWith typicalTolerance now verifier message signatures
-
--- | Verifies a webhook. This is the lowest-level function that gives you the
--- most control. If you're looking for something that's easier to use and
--- assumes some reasonable defaults, consider 'verifyWebhook'.
-verifyWebhookWith ::
-  -- | Often 'typicalTolerance'.
-  Tolerance.Tolerance ->
-  -- | Usually 'Time.getCurrentTime'.
-  Time.UTCTime ->
-  -- | See 'parseVerifier'.
-  Verifier.Verifier ->
-  -- | See 'parseMessage'. Or 'Message.MkMessage' along with 'parseId',
-  -- 'parseTimestamp', and 'Payload.MkPayload'.
-  Message.Message ->
-  -- | See 'parseSignatures'.
-  Signatures.Signatures ->
-  Either SignetException.SignetException Signature.Signature
-verifyWebhookWith tolerance now verifier message signatures = do
-  Bifunctor.first SignetException.ToleranceException
-    . Tolerance.check tolerance now
-    $ Message.timestamp message
-  Bifunctor.first SignetException.VerificationException $
-    Verifier.verify verifier message signatures
-
--- | Alias for 'Tolerance.typical'.
-typicalTolerance :: Tolerance.Tolerance
-typicalTolerance = Tolerance.typical
-
--- | Alias for 'Verifier.parse'.
-parseVerifier :: ByteString.ByteString -> Either InvalidVerifier.InvalidVerifier Verifier.Verifier
-parseVerifier = Verifier.parse
-
--- | Alias for 'Id.parse'.
-parseId :: ByteString.ByteString -> Either InvalidId.InvalidId Id.Id
-parseId = Id.parse
-
--- | Alias for 'Timestamp.parse'.
-parseTimestamp :: ByteString.ByteString -> Either InvalidTimestamp.InvalidTimestamp Timestamp.Timestamp
-parseTimestamp = Timestamp.parse
-
--- | Alias for 'Message.parse'.
-parseMessage :: ByteString.ByteString -> Either InvalidMessage.InvalidMessage Message.Message
-parseMessage = Message.parse
-
--- | Alias for 'Signatures.parse'.
-parseSignatures ::
-  ByteString.ByteString ->
-  Either InvalidSignature.InvalidSignature ([UnknownSignature.UnknownSignature], Signatures.Signatures)
-parseSignatures = Signatures.parse
-
--- | Alias for 'Verifier.Asymmetric'.
-pattern AsymmetricVerifier :: PublicKey.PublicKey -> Verifier.Verifier
-pattern AsymmetricVerifier publicKey = Verifier.Asymmetric publicKey
-
--- | Alias for 'Verifier.Symmetric'.
-pattern SymmetricVerifier :: Secret.Secret -> Verifier.Verifier
-pattern SymmetricVerifier secret = Verifier.Symmetric secret
-
-{-# COMPLETE AsymmetricVerifier, SymmetricVerifier #-}
-
--- | Alias for 'Signer.sign'.
-signWebhook :: Signer.Signer -> Message.Message -> Signature.Signature
-signWebhook = Signer.sign
-
--- | Alias for 'Signer.parse'.
-parseSigner :: ByteString.ByteString -> Either InvalidSigner.InvalidSigner Signer.Signer
-parseSigner = Signer.parse
-
--- | Alias for 'Signer.Asymmetric'.
-pattern AsymmetricSigner :: SecretKey.SecretKey -> Signer.Signer
-pattern AsymmetricSigner secretKey = Signer.Asymmetric secretKey
-
--- | Alias for 'Signer.Symmetric'.
-pattern SymmetricSigner :: Secret.Secret -> Signer.Signer
-pattern SymmetricSigner secret = Signer.Symmetric secret
-
-{-# COMPLETE AsymmetricSigner, SymmetricSigner #-}
-
--- | Alias for 'Signature.Asymmetric'.
-pattern AsymmetricSignature :: AsymmetricSignature.AsymmetricSignature -> Signature.Signature
-pattern AsymmetricSignature asymmetricSignature = Signature.Asymmetric asymmetricSignature
-
--- | Alias for 'Signature.Symmetric'.
-pattern SymmetricSignature :: SymmetricSignature.SymmetricSignature -> Signature.Signature
-pattern SymmetricSignature symmetricSignature = Signature.Symmetric symmetricSignature
-
-{-# COMPLETE AsymmetricSignature, SymmetricSignature #-}
diff --git a/source/libraries/unstable/Signet/Unstable/Exception/InvalidAsymmetricSignature.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidAsymmetricSignature.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Exception/InvalidAsymmetricSignature.hs
+++ /dev/null
@@ -1,14 +0,0 @@
-module Signet.Unstable.Exception.InvalidAsymmetricSignature where
-
-import qualified Control.Monad.Catch as Exception
-import qualified Data.ByteString as ByteString
-
-newtype InvalidAsymmetricSignature
-  = MkInvalidAsymmetricSignature ByteString.ByteString
-  deriving (Eq, Show)
-
-instance Exception.Exception InvalidAsymmetricSignature where
-  displayException = mappend "invalid asymmetric signature: " . show . unwrap
-
-unwrap :: InvalidAsymmetricSignature -> ByteString.ByteString
-unwrap (MkInvalidAsymmetricSignature byteString) = byteString
diff --git a/source/libraries/unstable/Signet/Unstable/Exception/InvalidId.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidId.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Exception/InvalidId.hs
+++ /dev/null
@@ -1,14 +0,0 @@
-module Signet.Unstable.Exception.InvalidId where
-
-import qualified Control.Monad.Catch as Exception
-import qualified Data.ByteString as ByteString
-
-newtype InvalidId
-  = MkInvalidId ByteString.ByteString
-  deriving (Eq, Show)
-
-instance Exception.Exception InvalidId where
-  displayException = mappend "invalid ID: " . show . unwrap
-
-unwrap :: InvalidId -> ByteString.ByteString
-unwrap (MkInvalidId byteString) = byteString
diff --git a/source/libraries/unstable/Signet/Unstable/Exception/InvalidMessage.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidMessage.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Exception/InvalidMessage.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module Signet.Unstable.Exception.InvalidMessage where
-
-import qualified Control.Monad.Catch as Exception
-import qualified Signet.Unstable.Exception.InvalidId as InvalidId
-import qualified Signet.Unstable.Exception.InvalidTimestamp as InvalidTimestamp
-
-data InvalidMessage
-  = InvalidId InvalidId.InvalidId
-  | InvalidTimestamp InvalidTimestamp.InvalidTimestamp
-  deriving (Eq, Show)
-
-instance Exception.Exception InvalidMessage where
-  displayException invalidMessage =
-    "invalid message: " <> case invalidMessage of
-      InvalidId invalidId ->
-        Exception.displayException invalidId
-      InvalidTimestamp invalidTimestamp ->
-        Exception.displayException invalidTimestamp
diff --git a/source/libraries/unstable/Signet/Unstable/Exception/InvalidPublicKey.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidPublicKey.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Exception/InvalidPublicKey.hs
+++ /dev/null
@@ -1,14 +0,0 @@
-module Signet.Unstable.Exception.InvalidPublicKey where
-
-import qualified Control.Monad.Catch as Exception
-import qualified Data.ByteString as ByteString
-
-newtype InvalidPublicKey
-  = MkInvalidPublicKey ByteString.ByteString
-  deriving (Eq, Show)
-
-instance Exception.Exception InvalidPublicKey where
-  displayException = mappend "invalid public key: " . show . unwrap
-
-unwrap :: InvalidPublicKey -> ByteString.ByteString
-unwrap (MkInvalidPublicKey byteString) = byteString
diff --git a/source/libraries/unstable/Signet/Unstable/Exception/InvalidSecret.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidSecret.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Exception/InvalidSecret.hs
+++ /dev/null
@@ -1,14 +0,0 @@
-module Signet.Unstable.Exception.InvalidSecret where
-
-import qualified Control.Monad.Catch as Exception
-import qualified Data.ByteString as ByteString
-
-newtype InvalidSecret
-  = MkInvalidSecret ByteString.ByteString
-  deriving (Eq, Show)
-
-instance Exception.Exception InvalidSecret where
-  displayException = mappend "invalid secret: " . show . unwrap
-
-unwrap :: InvalidSecret -> ByteString.ByteString
-unwrap (MkInvalidSecret byteString) = byteString
diff --git a/source/libraries/unstable/Signet/Unstable/Exception/InvalidSecretKey.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidSecretKey.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Exception/InvalidSecretKey.hs
+++ /dev/null
@@ -1,14 +0,0 @@
-module Signet.Unstable.Exception.InvalidSecretKey where
-
-import qualified Control.Monad.Catch as Exception
-import qualified Data.ByteString as ByteString
-
-newtype InvalidSecretKey
-  = MkInvalidSecretKey ByteString.ByteString
-  deriving (Eq, Show)
-
-instance Exception.Exception InvalidSecretKey where
-  displayException = mappend "invalid secret key: " . show . unwrap
-
-unwrap :: InvalidSecretKey -> ByteString.ByteString
-unwrap (MkInvalidSecretKey byteString) = byteString
diff --git a/source/libraries/unstable/Signet/Unstable/Exception/InvalidSignature.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidSignature.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Exception/InvalidSignature.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module Signet.Unstable.Exception.InvalidSignature where
-
-import qualified Control.Monad.Catch as Exception
-import qualified Signet.Unstable.Exception.InvalidAsymmetricSignature as InvalidAsymmetricSignature
-import qualified Signet.Unstable.Exception.InvalidSymmetricSignature as InvalidSymmetricSignature
-
-data InvalidSignature
-  = InvalidAsymmetricSignature InvalidAsymmetricSignature.InvalidAsymmetricSignature
-  | InvalidSymmetricSignature InvalidSymmetricSignature.InvalidSymmetricSignature
-  deriving (Eq, Show)
-
-instance Exception.Exception InvalidSignature where
-  displayException invalidSignature =
-    "invalid signature: " <> case invalidSignature of
-      InvalidAsymmetricSignature invalidAsymmetricSignature ->
-        Exception.displayException invalidAsymmetricSignature
-      InvalidSymmetricSignature invalidSymmetricSignature ->
-        Exception.displayException invalidSymmetricSignature
diff --git a/source/libraries/unstable/Signet/Unstable/Exception/InvalidSigner.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidSigner.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Exception/InvalidSigner.hs
+++ /dev/null
@@ -1,14 +0,0 @@
-module Signet.Unstable.Exception.InvalidSigner where
-
-import qualified Control.Monad.Catch as Exception
-import qualified Data.ByteString as ByteString
-
-newtype InvalidSigner
-  = MkInvalidSigner ByteString.ByteString
-  deriving (Eq, Show)
-
-instance Exception.Exception InvalidSigner where
-  displayException = mappend "invalid signer: " . show . unwrap
-
-unwrap :: InvalidSigner -> ByteString.ByteString
-unwrap (MkInvalidSigner byteString) = byteString
diff --git a/source/libraries/unstable/Signet/Unstable/Exception/InvalidSymmetricSignature.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidSymmetricSignature.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Exception/InvalidSymmetricSignature.hs
+++ /dev/null
@@ -1,14 +0,0 @@
-module Signet.Unstable.Exception.InvalidSymmetricSignature where
-
-import qualified Control.Monad.Catch as Exception
-import qualified Data.ByteString as ByteString
-
-newtype InvalidSymmetricSignature
-  = MkInvalidSymmetricSignature ByteString.ByteString
-  deriving (Eq, Show)
-
-instance Exception.Exception InvalidSymmetricSignature where
-  displayException = mappend "invalid symmetric signature: " . show . unwrap
-
-unwrap :: InvalidSymmetricSignature -> ByteString.ByteString
-unwrap (MkInvalidSymmetricSignature byteString) = byteString
diff --git a/source/libraries/unstable/Signet/Unstable/Exception/InvalidTimestamp.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidTimestamp.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Exception/InvalidTimestamp.hs
+++ /dev/null
@@ -1,14 +0,0 @@
-module Signet.Unstable.Exception.InvalidTimestamp where
-
-import qualified Control.Monad.Catch as Exception
-import qualified Data.ByteString as ByteString
-
-newtype InvalidTimestamp
-  = MkInvalidTimestamp ByteString.ByteString
-  deriving (Eq, Show)
-
-instance Exception.Exception InvalidTimestamp where
-  displayException = mappend "invalid timestamp: " . show . unwrap
-
-unwrap :: InvalidTimestamp -> ByteString.ByteString
-unwrap (MkInvalidTimestamp byteString) = byteString
diff --git a/source/libraries/unstable/Signet/Unstable/Exception/InvalidVerifier.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidVerifier.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Exception/InvalidVerifier.hs
+++ /dev/null
@@ -1,14 +0,0 @@
-module Signet.Unstable.Exception.InvalidVerifier where
-
-import qualified Control.Monad.Catch as Exception
-import qualified Data.ByteString as ByteString
-
-newtype InvalidVerifier
-  = MkInvalidVerifier ByteString.ByteString
-  deriving (Eq, Show)
-
-instance Exception.Exception InvalidVerifier where
-  displayException = mappend "invalid verifier: " . show . unwrap
-
-unwrap :: InvalidVerifier -> ByteString.ByteString
-unwrap (MkInvalidVerifier byteString) = byteString
diff --git a/source/libraries/unstable/Signet/Unstable/Exception/SignetException.hs b/source/libraries/unstable/Signet/Unstable/Exception/SignetException.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Exception/SignetException.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module Signet.Unstable.Exception.SignetException where
-
-import qualified Control.Monad.Catch as Exception
-import qualified Signet.Unstable.Exception.ToleranceException as ToleranceException
-import qualified Signet.Unstable.Exception.VerificationException as VerificationException
-
-data SignetException
-  = ToleranceException ToleranceException.ToleranceException
-  | VerificationException VerificationException.VerificationException
-  deriving (Eq, Show)
-
-instance Exception.Exception SignetException where
-  displayException signetException =
-    "failed to verify webhook: " <> case signetException of
-      ToleranceException toleranceException ->
-        Exception.displayException toleranceException
-      VerificationException verificationException ->
-        Exception.displayException verificationException
diff --git a/source/libraries/unstable/Signet/Unstable/Exception/ToleranceException.hs b/source/libraries/unstable/Signet/Unstable/Exception/ToleranceException.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Exception/ToleranceException.hs
+++ /dev/null
@@ -1,14 +0,0 @@
-module Signet.Unstable.Exception.ToleranceException where
-
-import qualified Control.Monad.Catch as Exception
-import qualified Signet.Unstable.Type.Timestamp as Timestamp
-
-newtype ToleranceException
-  = MkToleranceException Timestamp.Timestamp
-  deriving (Eq, Show)
-
-instance Exception.Exception ToleranceException where
-  displayException = mappend "timestamp out of tolerance: " . show . unwrap
-
-unwrap :: ToleranceException -> Timestamp.Timestamp
-unwrap (MkToleranceException timestamp) = timestamp
diff --git a/source/libraries/unstable/Signet/Unstable/Exception/VerificationException.hs b/source/libraries/unstable/Signet/Unstable/Exception/VerificationException.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Exception/VerificationException.hs
+++ /dev/null
@@ -1,14 +0,0 @@
-module Signet.Unstable.Exception.VerificationException where
-
-import qualified Control.Monad.Catch as Exception
-import qualified Signet.Unstable.Type.Id as Id
-
-newtype VerificationException
-  = MkVerificationException Id.Id
-  deriving (Eq, Show)
-
-instance Exception.Exception VerificationException where
-  displayException = mappend "verification failed: " . show . unwrap
-
-unwrap :: VerificationException -> Id.Id
-unwrap (MkVerificationException id_) = id_
diff --git a/source/libraries/unstable/Signet/Unstable/Extra/Either.hs b/source/libraries/unstable/Signet/Unstable/Extra/Either.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Extra/Either.hs
+++ /dev/null
@@ -1,9 +0,0 @@
-module Signet.Unstable.Extra.Either where
-
-import qualified Control.Monad.Catch as Exception
-
-hush :: Either x a -> Maybe a
-hush = either (const Nothing) Just
-
-throw :: (Exception.Exception e, Exception.MonadThrow m) => Either e a -> m a
-throw = either Exception.throwM pure
diff --git a/source/libraries/unstable/Signet/Unstable/Extra/Http.hs b/source/libraries/unstable/Signet/Unstable/Extra/Http.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Extra/Http.hs
+++ /dev/null
@@ -1,14 +0,0 @@
-module Signet.Unstable.Extra.Http where
-
-import qualified Data.ByteString.Char8 as Ascii
-import qualified Data.CaseInsensitive as CI
-import qualified Network.HTTP.Types as Http
-
-hWebhookId :: Http.HeaderName
-hWebhookId = CI.mk $ Ascii.pack "webhook-id"
-
-hWebhookSignature :: Http.HeaderName
-hWebhookSignature = CI.mk $ Ascii.pack "webhook-signature"
-
-hWebhookTimestamp :: Http.HeaderName
-hWebhookTimestamp = CI.mk $ Ascii.pack "webhook-timestamp"
diff --git a/source/libraries/unstable/Signet/Unstable/Extra/Maybe.hs b/source/libraries/unstable/Signet/Unstable/Extra/Maybe.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Extra/Maybe.hs
+++ /dev/null
@@ -1,4 +0,0 @@
-module Signet.Unstable.Extra.Maybe where
-
-note :: e -> Maybe a -> Either e a
-note = flip maybe Right . Left
diff --git a/source/libraries/unstable/Signet/Unstable/Type/AsymmetricSignature.hs b/source/libraries/unstable/Signet/Unstable/Type/AsymmetricSignature.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Type/AsymmetricSignature.hs
+++ /dev/null
@@ -1,28 +0,0 @@
-module Signet.Unstable.Type.AsymmetricSignature where
-
-import qualified Crypto.Error as Error
-import qualified Crypto.PubKey.Ed25519 as Ed25519
-import qualified Data.ByteArray.Encoding as Encoding
-import qualified Data.ByteString as ByteString
-import qualified Signet.Unstable.Exception.InvalidAsymmetricSignature as InvalidAsymmetricSignature
-import qualified Signet.Unstable.Extra.Either as Either
-import qualified Signet.Unstable.Extra.Maybe as Maybe
-
-newtype AsymmetricSignature
-  = MkAsymmetricSignature Ed25519.Signature
-  deriving (Eq, Show)
-
-unwrap :: AsymmetricSignature -> Ed25519.Signature
-unwrap (MkAsymmetricSignature signature) = signature
-
-parse ::
-  ByteString.ByteString ->
-  Either InvalidAsymmetricSignature.InvalidAsymmetricSignature AsymmetricSignature
-parse encoded = Maybe.note (InvalidAsymmetricSignature.MkInvalidAsymmetricSignature encoded) $ do
-  byteString <- Either.hush $ Encoding.convertFromBase Encoding.Base64 encoded
-  fmap MkAsymmetricSignature
-    . Error.maybeCryptoError
-    $ Ed25519.signature (byteString :: ByteString.ByteString)
-
-render :: AsymmetricSignature -> ByteString.ByteString
-render = Encoding.convertToBase Encoding.Base64 . unwrap
diff --git a/source/libraries/unstable/Signet/Unstable/Type/Id.hs b/source/libraries/unstable/Signet/Unstable/Type/Id.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Type/Id.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-module Signet.Unstable.Type.Id where
-
-import qualified Control.Monad as Monad
-import qualified Data.ByteString as ByteString
-import qualified Data.Word as Word
-import qualified Signet.Unstable.Exception.InvalidId as InvalidId
-
-newtype Id
-  = MkId ByteString.ByteString
-  deriving (Eq, Show)
-
-unwrap :: Id -> ByteString.ByteString
-unwrap (MkId byteString) = byteString
-
-separator :: Word.Word8
-separator = 0x2e
-
-parse :: ByteString.ByteString -> Either InvalidId.InvalidId Id
-parse byteString = do
-  Monad.when (ByteString.any (== separator) byteString)
-    . Left
-    $ InvalidId.MkInvalidId byteString
-  Right $ MkId byteString
-
-render :: Id -> ByteString.ByteString
-render = unwrap
diff --git a/source/libraries/unstable/Signet/Unstable/Type/Message.hs b/source/libraries/unstable/Signet/Unstable/Type/Message.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Type/Message.hs
+++ /dev/null
@@ -1,36 +0,0 @@
-module Signet.Unstable.Type.Message where
-
-import qualified Data.Bifunctor as Bifunctor
-import qualified Data.ByteString as ByteString
-import qualified Signet.Unstable.Exception.InvalidMessage as InvalidMessage
-import qualified Signet.Unstable.Type.Id as Id
-import qualified Signet.Unstable.Type.Payload as Payload
-import qualified Signet.Unstable.Type.Timestamp as Timestamp
-
-data Message = MkMessage
-  { id_ :: Id.Id,
-    timestamp :: Timestamp.Timestamp,
-    payload :: Payload.Payload
-  }
-  deriving (Eq, Show)
-
-parse :: ByteString.ByteString -> Either InvalidMessage.InvalidMessage Message
-parse x = do
-  let (rawId, y) = ByteString.break (== Id.separator) x
-  theId <- Bifunctor.first InvalidMessage.InvalidId $ Id.parse rawId
-  let (rawTimestamp, z) =
-        ByteString.break (== Id.separator) $
-          ByteString.drop 1 y
-  theTimestamp <-
-    Bifunctor.first InvalidMessage.InvalidTimestamp $
-      Timestamp.parse rawTimestamp
-  let thePayload = Payload.MkPayload $ ByteString.drop 1 z
-  pure MkMessage {id_ = theId, timestamp = theTimestamp, payload = thePayload}
-
-render :: Message -> ByteString.ByteString
-render message =
-  Id.render (id_ message)
-    <> ByteString.singleton Id.separator
-    <> Timestamp.render (timestamp message)
-    <> ByteString.singleton Id.separator
-    <> Payload.unwrap (payload message)
diff --git a/source/libraries/unstable/Signet/Unstable/Type/Payload.hs b/source/libraries/unstable/Signet/Unstable/Type/Payload.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Type/Payload.hs
+++ /dev/null
@@ -1,10 +0,0 @@
-module Signet.Unstable.Type.Payload where
-
-import qualified Data.ByteString as ByteString
-
-newtype Payload
-  = MkPayload ByteString.ByteString
-  deriving (Eq, Show)
-
-unwrap :: Payload -> ByteString.ByteString
-unwrap (MkPayload byteString) = byteString
diff --git a/source/libraries/unstable/Signet/Unstable/Type/PublicKey.hs b/source/libraries/unstable/Signet/Unstable/Type/PublicKey.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Type/PublicKey.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-module Signet.Unstable.Type.PublicKey where
-
-import qualified Crypto.Error as Error
-import qualified Crypto.PubKey.Ed25519 as Ed25519
-import qualified Data.ByteArray.Encoding as Encoding
-import qualified Data.ByteString as ByteString
-import qualified Data.ByteString.Char8 as Ascii
-import qualified Signet.Unstable.Exception.InvalidPublicKey as InvalidPublicKey
-import qualified Signet.Unstable.Extra.Either as Either
-import qualified Signet.Unstable.Extra.Maybe as Maybe
-
-newtype PublicKey
-  = MkPublicKey Ed25519.PublicKey
-  deriving (Eq, Show)
-
-unwrap :: PublicKey -> Ed25519.PublicKey
-unwrap (MkPublicKey publicKey) = publicKey
-
-prefix :: ByteString.ByteString
-prefix = Ascii.pack "whpk_"
-
-parse :: ByteString.ByteString -> Either InvalidPublicKey.InvalidPublicKey PublicKey
-parse prefixed = Maybe.note (InvalidPublicKey.MkInvalidPublicKey prefixed) $ do
-  encoded <- ByteString.stripPrefix prefix prefixed
-  byteString <- Either.hush $ Encoding.convertFromBase Encoding.Base64 encoded
-  fmap MkPublicKey
-    . Error.maybeCryptoError
-    $ Ed25519.publicKey (byteString :: ByteString.ByteString)
-
-render :: PublicKey -> ByteString.ByteString
-render = mappend prefix . Encoding.convertToBase Encoding.Base64 . unwrap
diff --git a/source/libraries/unstable/Signet/Unstable/Type/Secret.hs b/source/libraries/unstable/Signet/Unstable/Type/Secret.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Type/Secret.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-module Signet.Unstable.Type.Secret where
-
-import qualified Data.ByteArray as ByteArray
-import qualified Data.ByteArray.Encoding as Encoding
-import qualified Data.ByteString as ByteString
-import qualified Data.ByteString.Char8 as Ascii
-import qualified Signet.Unstable.Exception.InvalidSecret as InvalidSecret
-import qualified Signet.Unstable.Extra.Either as Either
-import qualified Signet.Unstable.Extra.Maybe as Maybe
-
-newtype Secret
-  = MkSecret ByteArray.ScrubbedBytes
-  deriving (Eq, Show)
-
-unwrap :: Secret -> ByteArray.ScrubbedBytes
-unwrap (MkSecret scrubbedBytes) = scrubbedBytes
-
-prefix :: ByteString.ByteString
-prefix = Ascii.pack "whsec_"
-
-parse :: ByteString.ByteString -> Either InvalidSecret.InvalidSecret Secret
-parse prefixed = Maybe.note (InvalidSecret.MkInvalidSecret prefixed) $ do
-  encoded <- ByteString.stripPrefix prefix prefixed
-  fmap MkSecret . Either.hush $ Encoding.convertFromBase Encoding.Base64 encoded
-
-render :: Secret -> ByteString.ByteString
-render = mappend prefix . Encoding.convertToBase Encoding.Base64 . unwrap
diff --git a/source/libraries/unstable/Signet/Unstable/Type/SecretKey.hs b/source/libraries/unstable/Signet/Unstable/Type/SecretKey.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Type/SecretKey.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-module Signet.Unstable.Type.SecretKey where
-
-import qualified Crypto.Error as Error
-import qualified Crypto.PubKey.Ed25519 as Ed25519
-import qualified Data.ByteArray.Encoding as Encoding
-import qualified Data.ByteString as ByteString
-import qualified Data.ByteString.Char8 as Ascii
-import qualified Signet.Unstable.Exception.InvalidSecretKey as InvalidSecretKey
-import qualified Signet.Unstable.Extra.Either as Either
-import qualified Signet.Unstable.Extra.Maybe as Maybe
-
-newtype SecretKey
-  = MkSecretKey Ed25519.SecretKey
-  deriving (Eq, Show)
-
-unwrap :: SecretKey -> Ed25519.SecretKey
-unwrap (MkSecretKey secretKey) = secretKey
-
-prefix :: ByteString.ByteString
-prefix = Ascii.pack "whsk_"
-
-parse :: ByteString.ByteString -> Either InvalidSecretKey.InvalidSecretKey SecretKey
-parse prefixed = Maybe.note (InvalidSecretKey.MkInvalidSecretKey prefixed) $ do
-  encoded <- ByteString.stripPrefix prefix prefixed
-  byteString <- Either.hush $ Encoding.convertFromBase Encoding.Base64 encoded
-  fmap MkSecretKey
-    . Error.maybeCryptoError
-    $ Ed25519.secretKey (byteString :: ByteString.ByteString)
-
-render :: SecretKey -> ByteString.ByteString
-render = mappend prefix . Encoding.convertToBase Encoding.Base64 . unwrap
diff --git a/source/libraries/unstable/Signet/Unstable/Type/Signature.hs b/source/libraries/unstable/Signet/Unstable/Type/Signature.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Type/Signature.hs
+++ /dev/null
@@ -1,49 +0,0 @@
-module Signet.Unstable.Type.Signature where
-
-import qualified Data.Bifunctor as Bifunctor
-import qualified Data.ByteString as ByteString
-import qualified Data.ByteString.Char8 as Ascii
-import qualified Data.Word as Word
-import qualified Signet.Unstable.Exception.InvalidSignature as InvalidSignature
-import qualified Signet.Unstable.Type.AsymmetricSignature as AsymmetricSignature
-import qualified Signet.Unstable.Type.SymmetricSignature as SymmetricSignature
-import qualified Signet.Unstable.Type.UnknownSignature as UnknownSignature
-
-data Signature
-  = Asymmetric AsymmetricSignature.AsymmetricSignature
-  | Symmetric SymmetricSignature.SymmetricSignature
-  deriving (Eq, Show)
-
-separator :: Word.Word8
-separator = 0x2c
-
-asymmetricPrefix :: ByteString.ByteString
-asymmetricPrefix = Ascii.pack "v1a"
-
-symmetricPrefix :: ByteString.ByteString
-symmetricPrefix = Ascii.pack "v1"
-
-parse :: ByteString.ByteString -> Either InvalidSignature.InvalidSignature (Either UnknownSignature.UnknownSignature Signature)
-parse prefixed = do
-  let (prefix, rest) = ByteString.break (== separator) prefixed
-  case ByteString.drop 1 rest of
-    byteString
-      | prefix == asymmetricPrefix ->
-          Bifunctor.bimap InvalidSignature.InvalidAsymmetricSignature (Right . Asymmetric) $
-            AsymmetricSignature.parse byteString
-      | prefix == symmetricPrefix ->
-          Bifunctor.bimap InvalidSignature.InvalidSymmetricSignature (Right . Symmetric) $
-            SymmetricSignature.parse byteString
-    _ -> Right . Left $ UnknownSignature.MkUnknownSignature prefixed
-
-render :: Signature -> ByteString.ByteString
-render signature =
-  case signature of
-    Asymmetric asymmetricSignature ->
-      asymmetricPrefix
-        <> ByteString.singleton separator
-        <> AsymmetricSignature.render asymmetricSignature
-    Symmetric symmetricSignature ->
-      symmetricPrefix
-        <> ByteString.singleton separator
-        <> SymmetricSignature.render symmetricSignature
diff --git a/source/libraries/unstable/Signet/Unstable/Type/Signatures.hs b/source/libraries/unstable/Signet/Unstable/Type/Signatures.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Type/Signatures.hs
+++ /dev/null
@@ -1,30 +0,0 @@
-module Signet.Unstable.Type.Signatures where
-
-import qualified Data.ByteString as ByteString
-import qualified Data.Either as Either
-import qualified Data.Word as Word
-import qualified Signet.Unstable.Exception.InvalidSignature as InvalidSignature
-import qualified Signet.Unstable.Type.Signature as Signature
-import qualified Signet.Unstable.Type.UnknownSignature as UnknownSignature
-
-newtype Signatures
-  = MkSignatures [Signature.Signature]
-  deriving (Eq, Show)
-
-unwrap :: Signatures -> [Signature.Signature]
-unwrap (MkSignatures signatures) = signatures
-
-separator :: Word.Word8
-separator = 0x20
-
-parse :: ByteString.ByteString -> Either InvalidSignature.InvalidSignature ([UnknownSignature.UnknownSignature], Signatures)
-parse =
-  fmap (fmap MkSignatures . Either.partitionEithers)
-    . traverse Signature.parse
-    . ByteString.split separator
-
-render :: Signatures -> ByteString.ByteString
-render =
-  ByteString.intercalate (ByteString.singleton separator)
-    . fmap Signature.render
-    . unwrap
diff --git a/source/libraries/unstable/Signet/Unstable/Type/Signer.hs b/source/libraries/unstable/Signet/Unstable/Type/Signer.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Type/Signer.hs
+++ /dev/null
@@ -1,49 +0,0 @@
-module Signet.Unstable.Type.Signer where
-
-import qualified Crypto.MAC.HMAC as Hmac
-import qualified Crypto.PubKey.Ed25519 as Ed25519
-import qualified Data.ByteString as ByteString
-import qualified Signet.Unstable.Exception.InvalidSigner as InvalidSigner
-import qualified Signet.Unstable.Type.AsymmetricSignature as AsymmetricSignature
-import qualified Signet.Unstable.Type.Message as Message
-import qualified Signet.Unstable.Type.Secret as Secret
-import qualified Signet.Unstable.Type.SecretKey as SecretKey
-import qualified Signet.Unstable.Type.Signature as Signature
-import qualified Signet.Unstable.Type.SymmetricSignature as SymmetricSignature
-
-data Signer
-  = Asymmetric SecretKey.SecretKey
-  | Symmetric Secret.Secret
-  deriving (Eq, Show)
-
-parse :: ByteString.ByteString -> Either InvalidSigner.InvalidSigner Signer
-parse byteString = case SecretKey.parse byteString of
-  Right secretKey -> Right $ Asymmetric secretKey
-  Left _ -> case Secret.parse byteString of
-    Right secret -> Right $ Symmetric secret
-    Left _ -> Left $ InvalidSigner.MkInvalidSigner byteString
-
-render :: Signer -> ByteString.ByteString
-render signer = case signer of
-  Asymmetric secretKey -> SecretKey.render secretKey
-  Symmetric secret -> Secret.render secret
-
-sign :: Signer -> Message.Message -> Signature.Signature
-sign signer = case signer of
-  Asymmetric secretKey -> Signature.Asymmetric . asymmetric secretKey
-  Symmetric secret -> Signature.Symmetric . symmetric secret
-
-asymmetric :: SecretKey.SecretKey -> Message.Message -> AsymmetricSignature.AsymmetricSignature
-asymmetric secretKey =
-  let sk = SecretKey.unwrap secretKey
-      pk = Ed25519.toPublic sk
-   in AsymmetricSignature.MkAsymmetricSignature
-        . Ed25519.sign sk pk
-        . Message.render
-
-symmetric :: Secret.Secret -> Message.Message -> SymmetricSignature.SymmetricSignature
-symmetric secret =
-  SymmetricSignature.MkSymmetricSignature
-    . Hmac.hmacGetDigest
-    . Hmac.hmac (Secret.unwrap secret)
-    . Message.render
diff --git a/source/libraries/unstable/Signet/Unstable/Type/SymmetricSignature.hs b/source/libraries/unstable/Signet/Unstable/Type/SymmetricSignature.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Type/SymmetricSignature.hs
+++ /dev/null
@@ -1,28 +0,0 @@
-module Signet.Unstable.Type.SymmetricSignature where
-
-import qualified Crypto.Hash as Hash
-import qualified Data.ByteArray as ByteArray
-import qualified Data.ByteArray.Encoding as Encoding
-import qualified Data.ByteString as ByteString
-import qualified Data.Function as Function
-import qualified Signet.Unstable.Exception.InvalidSymmetricSignature as InvalidSymmetricSignature
-import qualified Signet.Unstable.Extra.Either as Either
-import qualified Signet.Unstable.Extra.Maybe as Maybe
-
-newtype SymmetricSignature
-  = MkSymmetricSignature (Hash.Digest Hash.SHA256)
-  deriving (Show)
-
-instance Eq SymmetricSignature where
-  (==) = Function.on ByteArray.constEq unwrap
-
-unwrap :: SymmetricSignature -> Hash.Digest Hash.SHA256
-unwrap (MkSymmetricSignature digest) = digest
-
-parse :: ByteString.ByteString -> Either InvalidSymmetricSignature.InvalidSymmetricSignature SymmetricSignature
-parse encoded = Maybe.note (InvalidSymmetricSignature.MkInvalidSymmetricSignature encoded) $ do
-  byteString <- Either.hush $ Encoding.convertFromBase Encoding.Base64 encoded
-  MkSymmetricSignature <$> Hash.digestFromByteString (byteString :: ByteString.ByteString)
-
-render :: SymmetricSignature -> ByteString.ByteString
-render = Encoding.convertToBase Encoding.Base64 . unwrap
diff --git a/source/libraries/unstable/Signet/Unstable/Type/Timestamp.hs b/source/libraries/unstable/Signet/Unstable/Type/Timestamp.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Type/Timestamp.hs
+++ /dev/null
@@ -1,33 +0,0 @@
-module Signet.Unstable.Type.Timestamp where
-
-import qualified Data.ByteString as ByteString
-import qualified Data.Text as Text
-import qualified Data.Text.Encoding as Text
-import qualified Data.Time as Time
-import qualified Signet.Unstable.Exception.InvalidTimestamp as InvalidTimestamp
-import qualified Signet.Unstable.Extra.Either as Either
-import qualified Signet.Unstable.Extra.Maybe as Maybe
-
-newtype Timestamp
-  = MkTimestamp Time.UTCTime
-  deriving (Eq, Show)
-
-unwrap :: Timestamp -> Time.UTCTime
-unwrap (MkTimestamp utcTime) = utcTime
-
-format :: String
-format = "%s"
-
-parse :: ByteString.ByteString -> Either InvalidTimestamp.InvalidTimestamp Timestamp
-parse byteString = Maybe.note (InvalidTimestamp.MkInvalidTimestamp byteString) $ do
-  text <- Either.hush $ Text.decodeUtf8' byteString
-  fmap MkTimestamp
-    . Time.parseTimeM False Time.defaultTimeLocale format
-    $ Text.unpack text
-
-render :: Timestamp -> ByteString.ByteString
-render =
-  Text.encodeUtf8
-    . Text.pack
-    . Time.formatTime Time.defaultTimeLocale format
-    . unwrap
diff --git a/source/libraries/unstable/Signet/Unstable/Type/Tolerance.hs b/source/libraries/unstable/Signet/Unstable/Type/Tolerance.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Type/Tolerance.hs
+++ /dev/null
@@ -1,29 +0,0 @@
-module Signet.Unstable.Type.Tolerance where
-
-import qualified Control.Monad as Monad
-import qualified Data.Time as Time
-import qualified Signet.Unstable.Exception.ToleranceException as ToleranceException
-import qualified Signet.Unstable.Type.Timestamp as Timestamp
-
-newtype Tolerance
-  = MkTolerance Time.NominalDiffTime
-  deriving (Eq, Show)
-
-unwrap :: Tolerance -> Time.NominalDiffTime
-unwrap (MkTolerance nominalDiffTime) = nominalDiffTime
-
-typical :: Tolerance
-typical = MkTolerance 300
-
-check ::
-  Tolerance ->
-  Time.UTCTime ->
-  Timestamp.Timestamp ->
-  Either ToleranceException.ToleranceException ()
-check tolerance utcTime timestamp = do
-  let diff = Time.diffUTCTime utcTime $ Timestamp.unwrap timestamp
-  let hi = unwrap tolerance
-  let lo = negate hi
-  Monad.when (lo > diff || diff > hi)
-    . Left
-    $ ToleranceException.MkToleranceException timestamp
diff --git a/source/libraries/unstable/Signet/Unstable/Type/UnknownSignature.hs b/source/libraries/unstable/Signet/Unstable/Type/UnknownSignature.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Type/UnknownSignature.hs
+++ /dev/null
@@ -1,10 +0,0 @@
-module Signet.Unstable.Type.UnknownSignature where
-
-import qualified Data.ByteString as ByteString
-
-newtype UnknownSignature
-  = MkUnknownSignature ByteString.ByteString
-  deriving (Eq, Show)
-
-unwrap :: UnknownSignature -> ByteString.ByteString
-unwrap (MkUnknownSignature byteString) = byteString
diff --git a/source/libraries/unstable/Signet/Unstable/Type/Verifier.hs b/source/libraries/unstable/Signet/Unstable/Type/Verifier.hs
deleted file mode 100644
--- a/source/libraries/unstable/Signet/Unstable/Type/Verifier.hs
+++ /dev/null
@@ -1,75 +0,0 @@
-module Signet.Unstable.Type.Verifier where
-
-import qualified Crypto.PubKey.Ed25519 as Ed25519
-import qualified Data.ByteString as ByteString
-import qualified Data.List as List
-import qualified Data.Maybe as Maybe
-import qualified Signet.Unstable.Exception.InvalidVerifier as InvalidVerifier
-import qualified Signet.Unstable.Exception.VerificationException as VerificationException
-import qualified Signet.Unstable.Extra.Maybe as Maybe
-import qualified Signet.Unstable.Type.AsymmetricSignature as AsymmetricSignature
-import qualified Signet.Unstable.Type.Message as Message
-import qualified Signet.Unstable.Type.PublicKey as PublicKey
-import qualified Signet.Unstable.Type.Secret as Secret
-import qualified Signet.Unstable.Type.Signature as Signature
-import qualified Signet.Unstable.Type.Signatures as Signatures
-import qualified Signet.Unstable.Type.Signer as Signer
-import qualified Signet.Unstable.Type.SymmetricSignature as SymmetricSignature
-
-data Verifier
-  = Asymmetric PublicKey.PublicKey
-  | Symmetric Secret.Secret
-  deriving (Eq, Show)
-
-parse :: ByteString.ByteString -> Either InvalidVerifier.InvalidVerifier Verifier
-parse byteString = case PublicKey.parse byteString of
-  Right publicKey -> Right $ Asymmetric publicKey
-  Left _ -> case Secret.parse byteString of
-    Right secret -> Right $ Symmetric secret
-    Left _ -> Left $ InvalidVerifier.MkInvalidVerifier byteString
-
-render :: Verifier -> ByteString.ByteString
-render verifier = case verifier of
-  Asymmetric publicKey -> PublicKey.render publicKey
-  Symmetric secret -> Secret.render secret
-
-verify ::
-  Verifier ->
-  Message.Message ->
-  Signatures.Signatures ->
-  Either VerificationException.VerificationException Signature.Signature
-verify verifier message =
-  case verifier of
-    Asymmetric publicKey -> fmap Signature.Asymmetric . asymmetric publicKey message
-    Symmetric secret -> fmap Signature.Symmetric . symmetric secret message
-
-asymmetric ::
-  PublicKey.PublicKey ->
-  Message.Message ->
-  Signatures.Signatures ->
-  Either VerificationException.VerificationException AsymmetricSignature.AsymmetricSignature
-asymmetric publicKey message =
-  let toAsymmetric signature = case signature of
-        Signature.Asymmetric asymmetricSignature -> Just asymmetricSignature
-        Signature.Symmetric _ -> Nothing
-      pk = PublicKey.unwrap publicKey
-      bs = Message.render message
-   in Maybe.note (VerificationException.MkVerificationException $ Message.id_ message)
-        . List.find (Ed25519.verify pk bs . AsymmetricSignature.unwrap)
-        . Maybe.mapMaybe toAsymmetric
-        . Signatures.unwrap
-
-symmetric ::
-  Secret.Secret ->
-  Message.Message ->
-  Signatures.Signatures ->
-  Either VerificationException.VerificationException SymmetricSignature.SymmetricSignature
-symmetric secret message =
-  let toSymmetric signature = case signature of
-        Signature.Asymmetric _ -> Nothing
-        Signature.Symmetric symmetricSignature -> Just symmetricSignature
-      expected = Signer.symmetric secret message
-   in Maybe.note (VerificationException.MkVerificationException $ Message.id_ message)
-        . List.find (expected ==)
-        . Maybe.mapMaybe toSymmetric
-        . Signatures.unwrap
diff --git a/source/library/Signet.hs b/source/library/Signet.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet.hs
@@ -0,0 +1,88 @@
+module Signet
+  ( -- * Verification
+    Signet.Unstable.verifyWebhookText,
+    Signet.Unstable.verifyWebhookByteString,
+    Signet.Unstable.verifyWebhook,
+    Signet.Unstable.verifyWebhookWith,
+
+    -- * Signing
+    Signet.Unstable.signWebhook,
+
+    -- * Headers
+    Signet.Unstable.Extra.Http.hWebhookId,
+    Signet.Unstable.Extra.Http.hWebhookTimestamp,
+    Signet.Unstable.Extra.Http.hWebhookSignature,
+
+    -- * Types
+    Signet.Unstable.Type.AsymmetricSignature.AsymmetricSignature (..),
+    Signet.Unstable.Type.Id.Id (..),
+    Signet.Unstable.parseId,
+    Signet.Unstable.Type.Message.Message (..),
+    Signet.Unstable.parseMessage,
+    Signet.Unstable.Type.Payload.Payload (..),
+    Signet.Unstable.Type.PublicKey.PublicKey (..),
+    Signet.Unstable.Type.Secret.Secret (..),
+    Signet.Unstable.Type.SecretKey.SecretKey (..),
+    Signet.Unstable.Type.Signature.Signature (Signet.Unstable.AsymmetricSignature, Signet.Unstable.SymmetricSignature),
+    Signet.Unstable.Type.Signatures.Signatures (..),
+    Signet.Unstable.parseSignatures,
+    Signet.Unstable.Type.Signer.Signer (Signet.Unstable.AsymmetricSigner, Signet.Unstable.SymmetricSigner),
+    Signet.Unstable.parseSigner,
+    Signet.Unstable.Type.SymmetricSignature.SymmetricSignature (..),
+    Signet.Unstable.Type.Timestamp.Timestamp (..),
+    Signet.Unstable.parseTimestamp,
+    Signet.Unstable.Type.Tolerance.Tolerance (..),
+    Signet.Unstable.typicalTolerance,
+    Signet.Unstable.Type.UnknownSignature.UnknownSignature (..),
+    Signet.Unstable.Type.Verifier.Verifier (Signet.Unstable.AsymmetricVerifier, Signet.Unstable.SymmetricVerifier),
+    Signet.Unstable.parseVerifier,
+
+    -- * Exceptions
+    Signet.Unstable.Exception.InvalidAsymmetricSignature.InvalidAsymmetricSignature (..),
+    Signet.Unstable.Exception.InvalidId.InvalidId (..),
+    Signet.Unstable.Exception.InvalidMessage.InvalidMessage (..),
+    Signet.Unstable.Exception.InvalidPublicKey.InvalidPublicKey (..),
+    Signet.Unstable.Exception.InvalidSecret.InvalidSecret (..),
+    Signet.Unstable.Exception.InvalidSecretKey.InvalidSecretKey (..),
+    Signet.Unstable.Exception.InvalidSignature.InvalidSignature (..),
+    Signet.Unstable.Exception.InvalidSigner.InvalidSigner (..),
+    Signet.Unstable.Exception.InvalidSymmetricSignature.InvalidSymmetricSignature (..),
+    Signet.Unstable.Exception.InvalidTimestamp.InvalidTimestamp (..),
+    Signet.Unstable.Exception.InvalidVerifier.InvalidVerifier (..),
+    Signet.Unstable.Exception.SignetException.SignetException (..),
+    Signet.Unstable.Exception.ToleranceException.ToleranceException (..),
+    Signet.Unstable.Exception.VerificationException.VerificationException (..),
+  )
+where
+
+import qualified Signet.Unstable
+import qualified Signet.Unstable.Exception.InvalidAsymmetricSignature
+import qualified Signet.Unstable.Exception.InvalidId
+import qualified Signet.Unstable.Exception.InvalidMessage
+import qualified Signet.Unstable.Exception.InvalidPublicKey
+import qualified Signet.Unstable.Exception.InvalidSecret
+import qualified Signet.Unstable.Exception.InvalidSecretKey
+import qualified Signet.Unstable.Exception.InvalidSignature
+import qualified Signet.Unstable.Exception.InvalidSigner
+import qualified Signet.Unstable.Exception.InvalidSymmetricSignature
+import qualified Signet.Unstable.Exception.InvalidTimestamp
+import qualified Signet.Unstable.Exception.InvalidVerifier
+import qualified Signet.Unstable.Exception.SignetException
+import qualified Signet.Unstable.Exception.ToleranceException
+import qualified Signet.Unstable.Exception.VerificationException
+import qualified Signet.Unstable.Extra.Http
+import qualified Signet.Unstable.Type.AsymmetricSignature
+import qualified Signet.Unstable.Type.Id
+import qualified Signet.Unstable.Type.Message
+import qualified Signet.Unstable.Type.Payload
+import qualified Signet.Unstable.Type.PublicKey
+import qualified Signet.Unstable.Type.Secret
+import qualified Signet.Unstable.Type.SecretKey
+import qualified Signet.Unstable.Type.Signature
+import qualified Signet.Unstable.Type.Signatures
+import qualified Signet.Unstable.Type.Signer
+import qualified Signet.Unstable.Type.SymmetricSignature
+import qualified Signet.Unstable.Type.Timestamp
+import qualified Signet.Unstable.Type.Tolerance
+import qualified Signet.Unstable.Type.UnknownSignature
+import qualified Signet.Unstable.Type.Verifier
diff --git a/source/library/Signet/Unstable.hs b/source/library/Signet/Unstable.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable.hs
@@ -0,0 +1,196 @@
+{-# LANGUAGE PatternSynonyms #-}
+
+module Signet.Unstable where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Control.Monad.IO.Class as MonadIO
+import qualified Data.Bifunctor as Bifunctor
+import qualified Data.ByteString as ByteString
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Text
+import qualified Data.Time as Time
+import qualified Signet.Unstable.Exception.InvalidId as InvalidId
+import qualified Signet.Unstable.Exception.InvalidMessage as InvalidMessage
+import qualified Signet.Unstable.Exception.InvalidSignature as InvalidSignature
+import qualified Signet.Unstable.Exception.InvalidSigner as InvalidSigner
+import qualified Signet.Unstable.Exception.InvalidTimestamp as InvalidTimestamp
+import qualified Signet.Unstable.Exception.InvalidVerifier as InvalidVerifier
+import qualified Signet.Unstable.Exception.SignetException as SignetException
+import qualified Signet.Unstable.Extra.Either as Either
+import qualified Signet.Unstable.Type.AsymmetricSignature as AsymmetricSignature
+import qualified Signet.Unstable.Type.Id as Id
+import qualified Signet.Unstable.Type.Message as Message
+import qualified Signet.Unstable.Type.Payload as Payload
+import qualified Signet.Unstable.Type.PublicKey as PublicKey
+import qualified Signet.Unstable.Type.Secret as Secret
+import qualified Signet.Unstable.Type.SecretKey as SecretKey
+import qualified Signet.Unstable.Type.Signature as Signature
+import qualified Signet.Unstable.Type.Signatures as Signatures
+import qualified Signet.Unstable.Type.Signer as Signer
+import qualified Signet.Unstable.Type.SymmetricSignature as SymmetricSignature
+import qualified Signet.Unstable.Type.Timestamp as Timestamp
+import qualified Signet.Unstable.Type.Tolerance as Tolerance
+import qualified Signet.Unstable.Type.UnknownSignature as UnknownSignature
+import qualified Signet.Unstable.Type.Verifier as Verifier
+
+-- | Verifies a webhook with 'Text.Text' values. This is a wrapper around
+-- 'verifyWebhookByteString' that assumes all values are encoded as UTF-8.
+verifyWebhookText ::
+  (MonadIO.MonadIO m, Exception.MonadThrow m) =>
+  -- | A 'Verifier.Verifier' for the webhook. Typically this will be
+  -- @"whsec_..."@ or @"whpk_..."@.
+  Text.Text ->
+  -- | The webhook's unique 'Id.Id'. Typically this will come from the
+  -- 'Signet.Unstable.Extra.Http.hWebhookId' header and look like @"msg_..."@.
+  Text.Text ->
+  -- | The webhook's 'Timestamp.Timestamp'. This is an integer number of
+  -- seconds since the Unix epoch. Typically this will come from the
+  -- 'Signet.Unstable.Extra.Http.hWebhookTimestamp' header. For example
+  -- @"981173106"@ for @2001-02-03T04:05:06Z@.
+  Text.Text ->
+  -- | The webhook's raw 'Payload.Payload'. Typically this will be a JSON
+  -- object like @{"event_type":"ping",...}@.
+  Text.Text ->
+  -- | The webhook's 'Signatures.Signatures'. Typically this will come from the
+  -- 'Signet.Unstable.Extra.Http.hWebhookSignature' header and look like
+  -- @"v1_..."@ or @"v1a_..."@.
+  Text.Text ->
+  m Signature.Signature
+verifyWebhookText v i t p s =
+  verifyWebhookByteString
+    (Text.encodeUtf8 v)
+    (Text.encodeUtf8 i)
+    (Text.encodeUtf8 t)
+    (Text.encodeUtf8 p)
+    (Text.encodeUtf8 s)
+
+-- | Verifies a webhook with 'ByteString.ByteString' values. This is a
+-- wrapper around 'verifyWebhook'. See 'verifyWebhookText' for a description of
+-- the arguments.
+verifyWebhookByteString ::
+  (MonadIO.MonadIO m, Exception.MonadThrow m) =>
+  -- | 'Verifier.Verifier'
+  ByteString.ByteString ->
+  -- | 'Id.Id'
+  ByteString.ByteString ->
+  -- | 'Timestamp.Timestamp'
+  ByteString.ByteString ->
+  -- | 'Payload.Payload'
+  ByteString.ByteString ->
+  -- | 'Signatures.Signatures'
+  ByteString.ByteString ->
+  m Signature.Signature
+verifyWebhookByteString v i t p s = do
+  verifier <- Either.throw $ parseVerifier v
+  id_ <- Either.throw $ parseId i
+  timestamp <- Either.throw $ parseTimestamp t
+  let payload = Payload.MkPayload p
+  let message =
+        Message.MkMessage
+          { Message.id_ = id_,
+            Message.timestamp = timestamp,
+            Message.payload = payload
+          }
+  (_, signatures) <- Either.throw $ parseSignatures s
+  verifyWebhook verifier message signatures
+
+-- | Verifies a webhook. This is a wrapper around 'verifyWebhookWith' that uses
+-- 'typicalTolerance' and the current time.
+--
+-- Throws an exception if the webhook is invalid.
+verifyWebhook ::
+  (MonadIO.MonadIO m, Exception.MonadThrow m) =>
+  Verifier.Verifier ->
+  Message.Message ->
+  Signatures.Signatures ->
+  m Signature.Signature
+verifyWebhook verifier message signatures = do
+  now <- MonadIO.liftIO Time.getCurrentTime
+  Either.throw $ verifyWebhookWith typicalTolerance now verifier message signatures
+
+-- | Verifies a webhook. This is the lowest-level function that gives you the
+-- most control. If you're looking for something that's easier to use and
+-- assumes some reasonable defaults, consider 'verifyWebhook'.
+verifyWebhookWith ::
+  -- | Often 'typicalTolerance'.
+  Tolerance.Tolerance ->
+  -- | Usually 'Time.getCurrentTime'.
+  Time.UTCTime ->
+  -- | See 'parseVerifier'.
+  Verifier.Verifier ->
+  -- | See 'parseMessage'. Or 'Message.MkMessage' along with 'parseId',
+  -- 'parseTimestamp', and 'Payload.MkPayload'.
+  Message.Message ->
+  -- | See 'parseSignatures'.
+  Signatures.Signatures ->
+  Either SignetException.SignetException Signature.Signature
+verifyWebhookWith tolerance now verifier message signatures = do
+  Bifunctor.first SignetException.ToleranceException
+    . Tolerance.check tolerance now
+    $ Message.timestamp message
+  Bifunctor.first SignetException.VerificationException $
+    Verifier.verify verifier message signatures
+
+-- | Alias for 'Tolerance.typical'.
+typicalTolerance :: Tolerance.Tolerance
+typicalTolerance = Tolerance.typical
+
+-- | Alias for 'Verifier.parse'.
+parseVerifier :: ByteString.ByteString -> Either InvalidVerifier.InvalidVerifier Verifier.Verifier
+parseVerifier = Verifier.parse
+
+-- | Alias for 'Id.parse'.
+parseId :: ByteString.ByteString -> Either InvalidId.InvalidId Id.Id
+parseId = Id.parse
+
+-- | Alias for 'Timestamp.parse'.
+parseTimestamp :: ByteString.ByteString -> Either InvalidTimestamp.InvalidTimestamp Timestamp.Timestamp
+parseTimestamp = Timestamp.parse
+
+-- | Alias for 'Message.parse'.
+parseMessage :: ByteString.ByteString -> Either InvalidMessage.InvalidMessage Message.Message
+parseMessage = Message.parse
+
+-- | Alias for 'Signatures.parse'.
+parseSignatures ::
+  ByteString.ByteString ->
+  Either InvalidSignature.InvalidSignature ([UnknownSignature.UnknownSignature], Signatures.Signatures)
+parseSignatures = Signatures.parse
+
+-- | Alias for 'Verifier.Asymmetric'.
+pattern AsymmetricVerifier :: PublicKey.PublicKey -> Verifier.Verifier
+pattern AsymmetricVerifier publicKey = Verifier.Asymmetric publicKey
+
+-- | Alias for 'Verifier.Symmetric'.
+pattern SymmetricVerifier :: Secret.Secret -> Verifier.Verifier
+pattern SymmetricVerifier secret = Verifier.Symmetric secret
+
+{-# COMPLETE AsymmetricVerifier, SymmetricVerifier #-}
+
+-- | Alias for 'Signer.sign'.
+signWebhook :: Signer.Signer -> Message.Message -> Signature.Signature
+signWebhook = Signer.sign
+
+-- | Alias for 'Signer.parse'.
+parseSigner :: ByteString.ByteString -> Either InvalidSigner.InvalidSigner Signer.Signer
+parseSigner = Signer.parse
+
+-- | Alias for 'Signer.Asymmetric'.
+pattern AsymmetricSigner :: SecretKey.SecretKey -> Signer.Signer
+pattern AsymmetricSigner secretKey = Signer.Asymmetric secretKey
+
+-- | Alias for 'Signer.Symmetric'.
+pattern SymmetricSigner :: Secret.Secret -> Signer.Signer
+pattern SymmetricSigner secret = Signer.Symmetric secret
+
+{-# COMPLETE AsymmetricSigner, SymmetricSigner #-}
+
+-- | Alias for 'Signature.Asymmetric'.
+pattern AsymmetricSignature :: AsymmetricSignature.AsymmetricSignature -> Signature.Signature
+pattern AsymmetricSignature asymmetricSignature = Signature.Asymmetric asymmetricSignature
+
+-- | Alias for 'Signature.Symmetric'.
+pattern SymmetricSignature :: SymmetricSignature.SymmetricSignature -> Signature.Signature
+pattern SymmetricSignature symmetricSignature = Signature.Symmetric symmetricSignature
+
+{-# COMPLETE AsymmetricSignature, SymmetricSignature #-}
diff --git a/source/library/Signet/Unstable/Exception/InvalidAsymmetricSignature.hs b/source/library/Signet/Unstable/Exception/InvalidAsymmetricSignature.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidAsymmetricSignature.hs
@@ -0,0 +1,14 @@
+module Signet.Unstable.Exception.InvalidAsymmetricSignature where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Data.ByteString as ByteString
+
+newtype InvalidAsymmetricSignature
+  = MkInvalidAsymmetricSignature ByteString.ByteString
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidAsymmetricSignature where
+  displayException = mappend "invalid asymmetric signature: " . show . unwrap
+
+unwrap :: InvalidAsymmetricSignature -> ByteString.ByteString
+unwrap (MkInvalidAsymmetricSignature byteString) = byteString
diff --git a/source/library/Signet/Unstable/Exception/InvalidAsymmetricSignatureTest.hs b/source/library/Signet/Unstable/Exception/InvalidAsymmetricSignatureTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidAsymmetricSignatureTest.hs
@@ -0,0 +1,7 @@
+module Signet.Unstable.Exception.InvalidAsymmetricSignatureTest where
+
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Exception.InvalidAsymmetricSignature" $ do
+  pure ()
diff --git a/source/library/Signet/Unstable/Exception/InvalidId.hs b/source/library/Signet/Unstable/Exception/InvalidId.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidId.hs
@@ -0,0 +1,14 @@
+module Signet.Unstable.Exception.InvalidId where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Data.ByteString as ByteString
+
+newtype InvalidId
+  = MkInvalidId ByteString.ByteString
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidId where
+  displayException = mappend "invalid ID: " . show . unwrap
+
+unwrap :: InvalidId -> ByteString.ByteString
+unwrap (MkInvalidId byteString) = byteString
diff --git a/source/library/Signet/Unstable/Exception/InvalidIdTest.hs b/source/library/Signet/Unstable/Exception/InvalidIdTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidIdTest.hs
@@ -0,0 +1,7 @@
+module Signet.Unstable.Exception.InvalidIdTest where
+
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Exception.InvalidId" $ do
+  pure ()
diff --git a/source/library/Signet/Unstable/Exception/InvalidMessage.hs b/source/library/Signet/Unstable/Exception/InvalidMessage.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidMessage.hs
@@ -0,0 +1,18 @@
+module Signet.Unstable.Exception.InvalidMessage where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Signet.Unstable.Exception.InvalidId as InvalidId
+import qualified Signet.Unstable.Exception.InvalidTimestamp as InvalidTimestamp
+
+data InvalidMessage
+  = InvalidId InvalidId.InvalidId
+  | InvalidTimestamp InvalidTimestamp.InvalidTimestamp
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidMessage where
+  displayException invalidMessage =
+    "invalid message: " <> case invalidMessage of
+      InvalidId invalidId ->
+        Exception.displayException invalidId
+      InvalidTimestamp invalidTimestamp ->
+        Exception.displayException invalidTimestamp
diff --git a/source/library/Signet/Unstable/Exception/InvalidMessageTest.hs b/source/library/Signet/Unstable/Exception/InvalidMessageTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidMessageTest.hs
@@ -0,0 +1,7 @@
+module Signet.Unstable.Exception.InvalidMessageTest where
+
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Exception.InvalidMessage" $ do
+  pure ()
diff --git a/source/library/Signet/Unstable/Exception/InvalidPublicKey.hs b/source/library/Signet/Unstable/Exception/InvalidPublicKey.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidPublicKey.hs
@@ -0,0 +1,14 @@
+module Signet.Unstable.Exception.InvalidPublicKey where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Data.ByteString as ByteString
+
+newtype InvalidPublicKey
+  = MkInvalidPublicKey ByteString.ByteString
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidPublicKey where
+  displayException = mappend "invalid public key: " . show . unwrap
+
+unwrap :: InvalidPublicKey -> ByteString.ByteString
+unwrap (MkInvalidPublicKey byteString) = byteString
diff --git a/source/library/Signet/Unstable/Exception/InvalidPublicKeyTest.hs b/source/library/Signet/Unstable/Exception/InvalidPublicKeyTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidPublicKeyTest.hs
@@ -0,0 +1,7 @@
+module Signet.Unstable.Exception.InvalidPublicKeyTest where
+
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Exception.InvalidPublicKey" $ do
+  pure ()
diff --git a/source/library/Signet/Unstable/Exception/InvalidSecret.hs b/source/library/Signet/Unstable/Exception/InvalidSecret.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidSecret.hs
@@ -0,0 +1,14 @@
+module Signet.Unstable.Exception.InvalidSecret where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Data.ByteString as ByteString
+
+newtype InvalidSecret
+  = MkInvalidSecret ByteString.ByteString
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidSecret where
+  displayException = mappend "invalid secret: " . show . unwrap
+
+unwrap :: InvalidSecret -> ByteString.ByteString
+unwrap (MkInvalidSecret byteString) = byteString
diff --git a/source/library/Signet/Unstable/Exception/InvalidSecretKey.hs b/source/library/Signet/Unstable/Exception/InvalidSecretKey.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidSecretKey.hs
@@ -0,0 +1,14 @@
+module Signet.Unstable.Exception.InvalidSecretKey where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Data.ByteString as ByteString
+
+newtype InvalidSecretKey
+  = MkInvalidSecretKey ByteString.ByteString
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidSecretKey where
+  displayException = mappend "invalid secret key: " . show . unwrap
+
+unwrap :: InvalidSecretKey -> ByteString.ByteString
+unwrap (MkInvalidSecretKey byteString) = byteString
diff --git a/source/library/Signet/Unstable/Exception/InvalidSecretKeyTest.hs b/source/library/Signet/Unstable/Exception/InvalidSecretKeyTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidSecretKeyTest.hs
@@ -0,0 +1,7 @@
+module Signet.Unstable.Exception.InvalidSecretKeyTest where
+
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Exception.InvalidSecretKey" $ do
+  pure ()
diff --git a/source/library/Signet/Unstable/Exception/InvalidSecretTest.hs b/source/library/Signet/Unstable/Exception/InvalidSecretTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidSecretTest.hs
@@ -0,0 +1,7 @@
+module Signet.Unstable.Exception.InvalidSecretTest where
+
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Exception.InvalidSecret" $ do
+  pure ()
diff --git a/source/library/Signet/Unstable/Exception/InvalidSignature.hs b/source/library/Signet/Unstable/Exception/InvalidSignature.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidSignature.hs
@@ -0,0 +1,18 @@
+module Signet.Unstable.Exception.InvalidSignature where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Signet.Unstable.Exception.InvalidAsymmetricSignature as InvalidAsymmetricSignature
+import qualified Signet.Unstable.Exception.InvalidSymmetricSignature as InvalidSymmetricSignature
+
+data InvalidSignature
+  = InvalidAsymmetricSignature InvalidAsymmetricSignature.InvalidAsymmetricSignature
+  | InvalidSymmetricSignature InvalidSymmetricSignature.InvalidSymmetricSignature
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidSignature where
+  displayException invalidSignature =
+    "invalid signature: " <> case invalidSignature of
+      InvalidAsymmetricSignature invalidAsymmetricSignature ->
+        Exception.displayException invalidAsymmetricSignature
+      InvalidSymmetricSignature invalidSymmetricSignature ->
+        Exception.displayException invalidSymmetricSignature
diff --git a/source/library/Signet/Unstable/Exception/InvalidSignatureTest.hs b/source/library/Signet/Unstable/Exception/InvalidSignatureTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidSignatureTest.hs
@@ -0,0 +1,7 @@
+module Signet.Unstable.Exception.InvalidSignatureTest where
+
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Exception.InvalidSignature" $ do
+  pure ()
diff --git a/source/library/Signet/Unstable/Exception/InvalidSigner.hs b/source/library/Signet/Unstable/Exception/InvalidSigner.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidSigner.hs
@@ -0,0 +1,14 @@
+module Signet.Unstable.Exception.InvalidSigner where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Data.ByteString as ByteString
+
+newtype InvalidSigner
+  = MkInvalidSigner ByteString.ByteString
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidSigner where
+  displayException = mappend "invalid signer: " . show . unwrap
+
+unwrap :: InvalidSigner -> ByteString.ByteString
+unwrap (MkInvalidSigner byteString) = byteString
diff --git a/source/library/Signet/Unstable/Exception/InvalidSignerTest.hs b/source/library/Signet/Unstable/Exception/InvalidSignerTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidSignerTest.hs
@@ -0,0 +1,7 @@
+module Signet.Unstable.Exception.InvalidSignerTest where
+
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Exception.InvalidSigner" $ do
+  pure ()
diff --git a/source/library/Signet/Unstable/Exception/InvalidSymmetricSignature.hs b/source/library/Signet/Unstable/Exception/InvalidSymmetricSignature.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidSymmetricSignature.hs
@@ -0,0 +1,14 @@
+module Signet.Unstable.Exception.InvalidSymmetricSignature where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Data.ByteString as ByteString
+
+newtype InvalidSymmetricSignature
+  = MkInvalidSymmetricSignature ByteString.ByteString
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidSymmetricSignature where
+  displayException = mappend "invalid symmetric signature: " . show . unwrap
+
+unwrap :: InvalidSymmetricSignature -> ByteString.ByteString
+unwrap (MkInvalidSymmetricSignature byteString) = byteString
diff --git a/source/library/Signet/Unstable/Exception/InvalidSymmetricSignatureTest.hs b/source/library/Signet/Unstable/Exception/InvalidSymmetricSignatureTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidSymmetricSignatureTest.hs
@@ -0,0 +1,7 @@
+module Signet.Unstable.Exception.InvalidSymmetricSignatureTest where
+
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Exception.InvalidSymmetricSignature" $ do
+  pure ()
diff --git a/source/library/Signet/Unstable/Exception/InvalidTimestamp.hs b/source/library/Signet/Unstable/Exception/InvalidTimestamp.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidTimestamp.hs
@@ -0,0 +1,14 @@
+module Signet.Unstable.Exception.InvalidTimestamp where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Data.ByteString as ByteString
+
+newtype InvalidTimestamp
+  = MkInvalidTimestamp ByteString.ByteString
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidTimestamp where
+  displayException = mappend "invalid timestamp: " . show . unwrap
+
+unwrap :: InvalidTimestamp -> ByteString.ByteString
+unwrap (MkInvalidTimestamp byteString) = byteString
diff --git a/source/library/Signet/Unstable/Exception/InvalidTimestampTest.hs b/source/library/Signet/Unstable/Exception/InvalidTimestampTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidTimestampTest.hs
@@ -0,0 +1,7 @@
+module Signet.Unstable.Exception.InvalidTimestampTest where
+
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Exception.InvalidTimestamp" $ do
+  pure ()
diff --git a/source/library/Signet/Unstable/Exception/InvalidVerifier.hs b/source/library/Signet/Unstable/Exception/InvalidVerifier.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidVerifier.hs
@@ -0,0 +1,14 @@
+module Signet.Unstable.Exception.InvalidVerifier where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Data.ByteString as ByteString
+
+newtype InvalidVerifier
+  = MkInvalidVerifier ByteString.ByteString
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidVerifier where
+  displayException = mappend "invalid verifier: " . show . unwrap
+
+unwrap :: InvalidVerifier -> ByteString.ByteString
+unwrap (MkInvalidVerifier byteString) = byteString
diff --git a/source/library/Signet/Unstable/Exception/InvalidVerifierTest.hs b/source/library/Signet/Unstable/Exception/InvalidVerifierTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/InvalidVerifierTest.hs
@@ -0,0 +1,7 @@
+module Signet.Unstable.Exception.InvalidVerifierTest where
+
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Exception.InvalidVerifier" $ do
+  pure ()
diff --git a/source/library/Signet/Unstable/Exception/SignetException.hs b/source/library/Signet/Unstable/Exception/SignetException.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/SignetException.hs
@@ -0,0 +1,18 @@
+module Signet.Unstable.Exception.SignetException where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Signet.Unstable.Exception.ToleranceException as ToleranceException
+import qualified Signet.Unstable.Exception.VerificationException as VerificationException
+
+data SignetException
+  = ToleranceException ToleranceException.ToleranceException
+  | VerificationException VerificationException.VerificationException
+  deriving (Eq, Show)
+
+instance Exception.Exception SignetException where
+  displayException signetException =
+    "failed to verify webhook: " <> case signetException of
+      ToleranceException toleranceException ->
+        Exception.displayException toleranceException
+      VerificationException verificationException ->
+        Exception.displayException verificationException
diff --git a/source/library/Signet/Unstable/Exception/SignetExceptionTest.hs b/source/library/Signet/Unstable/Exception/SignetExceptionTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/SignetExceptionTest.hs
@@ -0,0 +1,7 @@
+module Signet.Unstable.Exception.SignetExceptionTest where
+
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Exception.SignetException" $ do
+  pure ()
diff --git a/source/library/Signet/Unstable/Exception/ToleranceException.hs b/source/library/Signet/Unstable/Exception/ToleranceException.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/ToleranceException.hs
@@ -0,0 +1,14 @@
+module Signet.Unstable.Exception.ToleranceException where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Signet.Unstable.Type.Timestamp as Timestamp
+
+newtype ToleranceException
+  = MkToleranceException Timestamp.Timestamp
+  deriving (Eq, Show)
+
+instance Exception.Exception ToleranceException where
+  displayException = mappend "timestamp out of tolerance: " . show . unwrap
+
+unwrap :: ToleranceException -> Timestamp.Timestamp
+unwrap (MkToleranceException timestamp) = timestamp
diff --git a/source/library/Signet/Unstable/Exception/ToleranceExceptionTest.hs b/source/library/Signet/Unstable/Exception/ToleranceExceptionTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/ToleranceExceptionTest.hs
@@ -0,0 +1,7 @@
+module Signet.Unstable.Exception.ToleranceExceptionTest where
+
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Exception.ToleranceException" $ do
+  pure ()
diff --git a/source/library/Signet/Unstable/Exception/VerificationException.hs b/source/library/Signet/Unstable/Exception/VerificationException.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/VerificationException.hs
@@ -0,0 +1,14 @@
+module Signet.Unstable.Exception.VerificationException where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Signet.Unstable.Type.Id as Id
+
+newtype VerificationException
+  = MkVerificationException Id.Id
+  deriving (Eq, Show)
+
+instance Exception.Exception VerificationException where
+  displayException = mappend "verification failed: " . show . unwrap
+
+unwrap :: VerificationException -> Id.Id
+unwrap (MkVerificationException id_) = id_
diff --git a/source/library/Signet/Unstable/Exception/VerificationExceptionTest.hs b/source/library/Signet/Unstable/Exception/VerificationExceptionTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Exception/VerificationExceptionTest.hs
@@ -0,0 +1,7 @@
+module Signet.Unstable.Exception.VerificationExceptionTest where
+
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Exception.VerificationException" $ do
+  pure ()
diff --git a/source/library/Signet/Unstable/Extra/Either.hs b/source/library/Signet/Unstable/Extra/Either.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Extra/Either.hs
@@ -0,0 +1,9 @@
+module Signet.Unstable.Extra.Either where
+
+import qualified Control.Monad.Catch as Exception
+
+hush :: Either x a -> Maybe a
+hush = either (const Nothing) Just
+
+throw :: (Exception.Exception e, Exception.MonadThrow m) => Either e a -> m a
+throw = either Exception.throwM pure
diff --git a/source/library/Signet/Unstable/Extra/EitherTest.hs b/source/library/Signet/Unstable/Extra/EitherTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Extra/EitherTest.hs
@@ -0,0 +1,30 @@
+module Signet.Unstable.Extra.EitherTest where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Data.Void as Void
+import qualified Signet.Unstable.Extra.Either as Either
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Extra.Either" $ do
+  Test.describe test "hush" $ do
+    Test.it test "works with Left" $ do
+      Test.assertEq test (Either.hush (Left () :: Either () Void.Void)) Nothing
+
+    Test.it test "works with Right" $ do
+      Test.assertEq test (Either.hush (Right () :: Either Void.Void ())) (Just ())
+
+  Test.describe test "throw" $ do
+    Test.it test "throws an exception for Left" $ do
+      result <- Exception.try . Either.throw $ Left MkTestException
+      Test.assertEq test result (Left MkTestException :: Either TestException Void.Void)
+
+    Test.it test "returns the value for Right" $ do
+      value <- Either.throw (Right () :: Either Void.Void ())
+      Test.assertEq test value ()
+
+data TestException
+  = MkTestException
+  deriving (Eq, Show)
+
+instance Exception.Exception TestException
diff --git a/source/library/Signet/Unstable/Extra/Http.hs b/source/library/Signet/Unstable/Extra/Http.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Extra/Http.hs
@@ -0,0 +1,14 @@
+module Signet.Unstable.Extra.Http where
+
+import qualified Data.ByteString.Char8 as Ascii
+import qualified Data.CaseInsensitive as CI
+import qualified Network.HTTP.Types as Http
+
+hWebhookId :: Http.HeaderName
+hWebhookId = CI.mk $ Ascii.pack "webhook-id"
+
+hWebhookSignature :: Http.HeaderName
+hWebhookSignature = CI.mk $ Ascii.pack "webhook-signature"
+
+hWebhookTimestamp :: Http.HeaderName
+hWebhookTimestamp = CI.mk $ Ascii.pack "webhook-timestamp"
diff --git a/source/library/Signet/Unstable/Extra/HttpTest.hs b/source/library/Signet/Unstable/Extra/HttpTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Extra/HttpTest.hs
@@ -0,0 +1,20 @@
+module Signet.Unstable.Extra.HttpTest where
+
+import qualified Data.ByteString.Char8 as Ascii
+import qualified Data.CaseInsensitive as CI
+import qualified Signet.Unstable.Extra.Http as Http
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Extra.Http" $ do
+  Test.describe test "hWebhookId" $ do
+    Test.it test "creates the correct header name" $ do
+      Test.assertEq test Http.hWebhookId (CI.mk (Ascii.pack "webhook-id"))
+
+  Test.describe test "hWebhookSignature" $ do
+    Test.it test "creates the correct header name" $ do
+      Test.assertEq test Http.hWebhookSignature (CI.mk (Ascii.pack "webhook-signature"))
+
+  Test.describe test "hWebhookTimestamp" $ do
+    Test.it test "creates the correct header name" $ do
+      Test.assertEq test Http.hWebhookTimestamp (CI.mk (Ascii.pack "webhook-timestamp"))
diff --git a/source/library/Signet/Unstable/Extra/Maybe.hs b/source/library/Signet/Unstable/Extra/Maybe.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Extra/Maybe.hs
@@ -0,0 +1,4 @@
+module Signet.Unstable.Extra.Maybe where
+
+note :: e -> Maybe a -> Either e a
+note = flip maybe Right . Left
diff --git a/source/library/Signet/Unstable/Extra/MaybeTest.hs b/source/library/Signet/Unstable/Extra/MaybeTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Extra/MaybeTest.hs
@@ -0,0 +1,14 @@
+module Signet.Unstable.Extra.MaybeTest where
+
+import qualified Data.Void as Void
+import qualified Signet.Unstable.Extra.Maybe as Maybe
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Extra.Maybe" $ do
+  Test.describe test "note" $ do
+    Test.it test "works with nothing" $ do
+      Test.assertEq test (Maybe.note () (Nothing :: Maybe Void.Void)) (Left ())
+
+    Test.it test "works with just" $ do
+      Test.assertEq test (Maybe.note () (Just True)) (Right True)
diff --git a/source/library/Signet/Unstable/Type/AsymmetricSignature.hs b/source/library/Signet/Unstable/Type/AsymmetricSignature.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/AsymmetricSignature.hs
@@ -0,0 +1,28 @@
+module Signet.Unstable.Type.AsymmetricSignature where
+
+import qualified Crypto.Error as Error
+import qualified Crypto.PubKey.Ed25519 as Ed25519
+import qualified Data.ByteArray.Encoding as Encoding
+import qualified Data.ByteString as ByteString
+import qualified Signet.Unstable.Exception.InvalidAsymmetricSignature as InvalidAsymmetricSignature
+import qualified Signet.Unstable.Extra.Either as Either
+import qualified Signet.Unstable.Extra.Maybe as Maybe
+
+newtype AsymmetricSignature
+  = MkAsymmetricSignature Ed25519.Signature
+  deriving (Eq, Show)
+
+unwrap :: AsymmetricSignature -> Ed25519.Signature
+unwrap (MkAsymmetricSignature signature) = signature
+
+parse ::
+  ByteString.ByteString ->
+  Either InvalidAsymmetricSignature.InvalidAsymmetricSignature AsymmetricSignature
+parse encoded = Maybe.note (InvalidAsymmetricSignature.MkInvalidAsymmetricSignature encoded) $ do
+  byteString <- Either.hush $ Encoding.convertFromBase Encoding.Base64 encoded
+  fmap MkAsymmetricSignature
+    . Error.maybeCryptoError
+    $ Ed25519.signature (byteString :: ByteString.ByteString)
+
+render :: AsymmetricSignature -> ByteString.ByteString
+render = Encoding.convertToBase Encoding.Base64 . unwrap
diff --git a/source/library/Signet/Unstable/Type/AsymmetricSignatureTest.hs b/source/library/Signet/Unstable/Type/AsymmetricSignatureTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/AsymmetricSignatureTest.hs
@@ -0,0 +1,26 @@
+module Signet.Unstable.Type.AsymmetricSignatureTest where
+
+import qualified Crypto.Error as Error
+import qualified Crypto.PubKey.Ed25519 as Ed25519
+import qualified Data.ByteString.Char8 as Ascii
+import qualified Signet.Unstable.Exception.InvalidAsymmetricSignature as InvalidAsymmetricSignature
+import qualified Signet.Unstable.Type.AsymmetricSignature as AsymmetricSignature
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Type.AsymmetricSignature" $ do
+  Test.describe test "parse" $ do
+    Test.it test "fails with invalid input" $ do
+      let byteString = Ascii.pack "invalid"
+      let result = AsymmetricSignature.parse byteString
+      Test.assertEq test result (Left $ InvalidAsymmetricSignature.MkInvalidAsymmetricSignature byteString)
+
+    Test.it test "succeeds with valid input" $ do
+      let result = AsymmetricSignature.parse $ Ascii.pack "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVotMDEyMzQ1Njc4OS1hYmNkZWZnaGlqa2xtbm9wcXJzdHV2cXh5eg=="
+      signature <- Error.throwCryptoErrorIO . Ed25519.signature $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789-abcdefghijklmnopqrstuvqxyz"
+      Test.assertEq test result (Right $ AsymmetricSignature.MkAsymmetricSignature signature)
+
+  Test.describe test "render" $ do
+    Test.it test "works" $ do
+      asymmetricSignature <- fmap AsymmetricSignature.MkAsymmetricSignature . Error.throwCryptoErrorIO . Ed25519.signature $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789-abcdefghijklmnopqrstuvqxyz"
+      Test.assertEq test (AsymmetricSignature.render asymmetricSignature) (Ascii.pack "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVotMDEyMzQ1Njc4OS1hYmNkZWZnaGlqa2xtbm9wcXJzdHV2cXh5eg==")
diff --git a/source/library/Signet/Unstable/Type/Id.hs b/source/library/Signet/Unstable/Type/Id.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/Id.hs
@@ -0,0 +1,26 @@
+module Signet.Unstable.Type.Id where
+
+import qualified Control.Monad as Monad
+import qualified Data.ByteString as ByteString
+import qualified Data.Word as Word
+import qualified Signet.Unstable.Exception.InvalidId as InvalidId
+
+newtype Id
+  = MkId ByteString.ByteString
+  deriving (Eq, Show)
+
+unwrap :: Id -> ByteString.ByteString
+unwrap (MkId byteString) = byteString
+
+separator :: Word.Word8
+separator = 0x2e
+
+parse :: ByteString.ByteString -> Either InvalidId.InvalidId Id
+parse byteString = do
+  Monad.when (ByteString.any (== separator) byteString)
+    . Left
+    $ InvalidId.MkInvalidId byteString
+  Right $ MkId byteString
+
+render :: Id -> ByteString.ByteString
+render = unwrap
diff --git a/source/library/Signet/Unstable/Type/IdTest.hs b/source/library/Signet/Unstable/Type/IdTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/IdTest.hs
@@ -0,0 +1,25 @@
+module Signet.Unstable.Type.IdTest where
+
+import qualified Data.ByteString.Char8 as Ascii
+import qualified Signet.Unstable.Exception.InvalidId as InvalidId
+import qualified Signet.Unstable.Type.Id as Id
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Type.Id" $ do
+  Test.describe test "parse" $ do
+    Test.it test "fails with input containing separator" $ do
+      let byteString = Ascii.pack "invalid.id"
+      let result = Id.parse byteString
+      Test.assertEq test result (Left $ InvalidId.MkInvalidId byteString)
+
+    Test.it test "succeeds with valid input" $ do
+      let byteString = Ascii.pack "valid-id"
+      let result = Id.parse byteString
+      Test.assertEq test result (Right $ Id.MkId byteString)
+
+  Test.describe test "render" $ do
+    Test.it test "returns the original ByteString" $ do
+      let byteString = Ascii.pack "valid-id"
+      let id_ = Id.MkId byteString
+      Test.assertEq test (Id.render id_) byteString
diff --git a/source/library/Signet/Unstable/Type/Message.hs b/source/library/Signet/Unstable/Type/Message.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/Message.hs
@@ -0,0 +1,36 @@
+module Signet.Unstable.Type.Message where
+
+import qualified Data.Bifunctor as Bifunctor
+import qualified Data.ByteString as ByteString
+import qualified Signet.Unstable.Exception.InvalidMessage as InvalidMessage
+import qualified Signet.Unstable.Type.Id as Id
+import qualified Signet.Unstable.Type.Payload as Payload
+import qualified Signet.Unstable.Type.Timestamp as Timestamp
+
+data Message = MkMessage
+  { id_ :: Id.Id,
+    timestamp :: Timestamp.Timestamp,
+    payload :: Payload.Payload
+  }
+  deriving (Eq, Show)
+
+parse :: ByteString.ByteString -> Either InvalidMessage.InvalidMessage Message
+parse x = do
+  let (rawId, y) = ByteString.break (== Id.separator) x
+  theId <- Bifunctor.first InvalidMessage.InvalidId $ Id.parse rawId
+  let (rawTimestamp, z) =
+        ByteString.break (== Id.separator) $
+          ByteString.drop 1 y
+  theTimestamp <-
+    Bifunctor.first InvalidMessage.InvalidTimestamp $
+      Timestamp.parse rawTimestamp
+  let thePayload = Payload.MkPayload $ ByteString.drop 1 z
+  pure MkMessage {id_ = theId, timestamp = theTimestamp, payload = thePayload}
+
+render :: Message -> ByteString.ByteString
+render message =
+  Id.render (id_ message)
+    <> ByteString.singleton Id.separator
+    <> Timestamp.render (timestamp message)
+    <> ByteString.singleton Id.separator
+    <> Payload.unwrap (payload message)
diff --git a/source/library/Signet/Unstable/Type/MessageTest.hs b/source/library/Signet/Unstable/Type/MessageTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/MessageTest.hs
@@ -0,0 +1,49 @@
+module Signet.Unstable.Type.MessageTest where
+
+import qualified Data.ByteString.Char8 as Ascii
+import qualified Signet.Unstable.Exception.InvalidMessage as InvalidMessage
+import qualified Signet.Unstable.Exception.InvalidTimestamp as InvalidTimestamp
+import qualified Signet.Unstable.Extra.Either as Either
+import qualified Signet.Unstable.Type.Id as Id
+import qualified Signet.Unstable.Type.Message as Message
+import qualified Signet.Unstable.Type.Payload as Payload
+import qualified Signet.Unstable.Type.Test as Test
+import qualified Signet.Unstable.Type.Timestamp as Timestamp
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Type.Message" $ do
+  Test.describe test "parse" $ do
+    Test.it test "fails with invalid timestamp" $ do
+      let timestamp = Ascii.pack "invalid"
+      let byteString = Ascii.pack "i." <> timestamp <> Ascii.pack ".p"
+      let result = Message.parse byteString
+      Test.assertEq test result (Left (InvalidMessage.InvalidTimestamp $ InvalidTimestamp.MkInvalidTimestamp timestamp))
+
+    Test.it test "succeeds with valid input" $ do
+      let result = Message.parse $ Ascii.pack "i.0.p"
+      id_ <- Either.throw . Id.parse $ Ascii.pack "i"
+      timestamp <- Either.throw . Timestamp.parse $ Ascii.pack "0"
+      let payload = Payload.MkPayload $ Ascii.pack "p"
+      Test.assertEq
+        test
+        result
+        ( Right
+            Message.MkMessage
+              { Message.id_ = id_,
+                Message.timestamp = timestamp,
+                Message.payload = payload
+              }
+        )
+
+  Test.describe test "render" $ do
+    Test.it test "returns the correct ByteString representation" $ do
+      id_ <- Either.throw . Id.parse $ Ascii.pack "i"
+      timestamp <- Either.throw . Timestamp.parse $ Ascii.pack "0"
+      let payload = Payload.MkPayload $ Ascii.pack "p"
+      let message =
+            Message.MkMessage
+              { Message.id_ = id_,
+                Message.timestamp = timestamp,
+                Message.payload = payload
+              }
+      Test.assertEq test (Message.render message) (Ascii.pack "i.0.p")
diff --git a/source/library/Signet/Unstable/Type/Payload.hs b/source/library/Signet/Unstable/Type/Payload.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/Payload.hs
@@ -0,0 +1,10 @@
+module Signet.Unstable.Type.Payload where
+
+import qualified Data.ByteString as ByteString
+
+newtype Payload
+  = MkPayload ByteString.ByteString
+  deriving (Eq, Show)
+
+unwrap :: Payload -> ByteString.ByteString
+unwrap (MkPayload byteString) = byteString
diff --git a/source/library/Signet/Unstable/Type/PayloadTest.hs b/source/library/Signet/Unstable/Type/PayloadTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/PayloadTest.hs
@@ -0,0 +1,7 @@
+module Signet.Unstable.Type.PayloadTest where
+
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Type.Payload" $ do
+  pure ()
diff --git a/source/library/Signet/Unstable/Type/PublicKey.hs b/source/library/Signet/Unstable/Type/PublicKey.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/PublicKey.hs
@@ -0,0 +1,31 @@
+module Signet.Unstable.Type.PublicKey where
+
+import qualified Crypto.Error as Error
+import qualified Crypto.PubKey.Ed25519 as Ed25519
+import qualified Data.ByteArray.Encoding as Encoding
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Char8 as Ascii
+import qualified Signet.Unstable.Exception.InvalidPublicKey as InvalidPublicKey
+import qualified Signet.Unstable.Extra.Either as Either
+import qualified Signet.Unstable.Extra.Maybe as Maybe
+
+newtype PublicKey
+  = MkPublicKey Ed25519.PublicKey
+  deriving (Eq, Show)
+
+unwrap :: PublicKey -> Ed25519.PublicKey
+unwrap (MkPublicKey publicKey) = publicKey
+
+prefix :: ByteString.ByteString
+prefix = Ascii.pack "whpk_"
+
+parse :: ByteString.ByteString -> Either InvalidPublicKey.InvalidPublicKey PublicKey
+parse prefixed = Maybe.note (InvalidPublicKey.MkInvalidPublicKey prefixed) $ do
+  encoded <- ByteString.stripPrefix prefix prefixed
+  byteString <- Either.hush $ Encoding.convertFromBase Encoding.Base64 encoded
+  fmap MkPublicKey
+    . Error.maybeCryptoError
+    $ Ed25519.publicKey (byteString :: ByteString.ByteString)
+
+render :: PublicKey -> ByteString.ByteString
+render = mappend prefix . Encoding.convertToBase Encoding.Base64 . unwrap
diff --git a/source/library/Signet/Unstable/Type/PublicKeyTest.hs b/source/library/Signet/Unstable/Type/PublicKeyTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/PublicKeyTest.hs
@@ -0,0 +1,31 @@
+module Signet.Unstable.Type.PublicKeyTest where
+
+import qualified Crypto.Error as Error
+import qualified Crypto.PubKey.Ed25519 as Ed25519
+import qualified Data.ByteString.Char8 as Ascii
+import qualified Signet.Unstable.Exception.InvalidPublicKey as InvalidPublicKey
+import qualified Signet.Unstable.Type.PublicKey as PublicKey
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Type.PublicKey" $ do
+  Test.describe test "parse" $ do
+    Test.it test "fails with invalid prefix" $ do
+      let byteString = Ascii.pack "invalid"
+      let result = PublicKey.parse byteString
+      Test.assertEq test result (Left (InvalidPublicKey.MkInvalidPublicKey byteString))
+
+    Test.it test "fails with invalid input" $ do
+      let byteString = Ascii.pack "whpk_invalid"
+      let result = PublicKey.parse byteString
+      Test.assertEq test result (Left (InvalidPublicKey.MkInvalidPublicKey byteString))
+
+    Test.it test "succeeds with valid input" $ do
+      let result = PublicKey.parse $ Ascii.pack "whpk_QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU="
+      publicKey <- fmap PublicKey.MkPublicKey . Error.throwCryptoErrorIO . Ed25519.publicKey $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"
+      Test.assertEq test result (Right publicKey)
+
+  Test.describe test "render" $ do
+    Test.it test "works" $ do
+      publicKey <- fmap PublicKey.MkPublicKey . Error.throwCryptoErrorIO . Ed25519.publicKey $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"
+      Test.assertEq test (PublicKey.render publicKey) (Ascii.pack "whpk_QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU=")
diff --git a/source/library/Signet/Unstable/Type/Secret.hs b/source/library/Signet/Unstable/Type/Secret.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/Secret.hs
@@ -0,0 +1,27 @@
+module Signet.Unstable.Type.Secret where
+
+import qualified Data.ByteArray as ByteArray
+import qualified Data.ByteArray.Encoding as Encoding
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Char8 as Ascii
+import qualified Signet.Unstable.Exception.InvalidSecret as InvalidSecret
+import qualified Signet.Unstable.Extra.Either as Either
+import qualified Signet.Unstable.Extra.Maybe as Maybe
+
+newtype Secret
+  = MkSecret ByteArray.ScrubbedBytes
+  deriving (Eq, Show)
+
+unwrap :: Secret -> ByteArray.ScrubbedBytes
+unwrap (MkSecret scrubbedBytes) = scrubbedBytes
+
+prefix :: ByteString.ByteString
+prefix = Ascii.pack "whsec_"
+
+parse :: ByteString.ByteString -> Either InvalidSecret.InvalidSecret Secret
+parse prefixed = Maybe.note (InvalidSecret.MkInvalidSecret prefixed) $ do
+  encoded <- ByteString.stripPrefix prefix prefixed
+  fmap MkSecret . Either.hush $ Encoding.convertFromBase Encoding.Base64 encoded
+
+render :: Secret -> ByteString.ByteString
+render = mappend prefix . Encoding.convertToBase Encoding.Base64 . unwrap
diff --git a/source/library/Signet/Unstable/Type/SecretKey.hs b/source/library/Signet/Unstable/Type/SecretKey.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/SecretKey.hs
@@ -0,0 +1,31 @@
+module Signet.Unstable.Type.SecretKey where
+
+import qualified Crypto.Error as Error
+import qualified Crypto.PubKey.Ed25519 as Ed25519
+import qualified Data.ByteArray.Encoding as Encoding
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Char8 as Ascii
+import qualified Signet.Unstable.Exception.InvalidSecretKey as InvalidSecretKey
+import qualified Signet.Unstable.Extra.Either as Either
+import qualified Signet.Unstable.Extra.Maybe as Maybe
+
+newtype SecretKey
+  = MkSecretKey Ed25519.SecretKey
+  deriving (Eq, Show)
+
+unwrap :: SecretKey -> Ed25519.SecretKey
+unwrap (MkSecretKey secretKey) = secretKey
+
+prefix :: ByteString.ByteString
+prefix = Ascii.pack "whsk_"
+
+parse :: ByteString.ByteString -> Either InvalidSecretKey.InvalidSecretKey SecretKey
+parse prefixed = Maybe.note (InvalidSecretKey.MkInvalidSecretKey prefixed) $ do
+  encoded <- ByteString.stripPrefix prefix prefixed
+  byteString <- Either.hush $ Encoding.convertFromBase Encoding.Base64 encoded
+  fmap MkSecretKey
+    . Error.maybeCryptoError
+    $ Ed25519.secretKey (byteString :: ByteString.ByteString)
+
+render :: SecretKey -> ByteString.ByteString
+render = mappend prefix . Encoding.convertToBase Encoding.Base64 . unwrap
diff --git a/source/library/Signet/Unstable/Type/SecretKeyTest.hs b/source/library/Signet/Unstable/Type/SecretKeyTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/SecretKeyTest.hs
@@ -0,0 +1,31 @@
+module Signet.Unstable.Type.SecretKeyTest where
+
+import qualified Crypto.Error as Error
+import qualified Crypto.PubKey.Ed25519 as Ed25519
+import qualified Data.ByteString.Char8 as Ascii
+import qualified Signet.Unstable.Exception.InvalidSecretKey as InvalidSecretKey
+import qualified Signet.Unstable.Type.SecretKey as SecretKey
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Type.SecretKey" $ do
+  Test.describe test "parse" $ do
+    Test.it test "fails with invalid prefix" $ do
+      let byteString = Ascii.pack "invalid"
+      let result = SecretKey.parse byteString
+      Test.assertEq test result (Left (InvalidSecretKey.MkInvalidSecretKey byteString))
+
+    Test.it test "fails with invalid input" $ do
+      let byteString = Ascii.pack "whsk_invalid"
+      let result = SecretKey.parse byteString
+      Test.assertEq test result (Left (InvalidSecretKey.MkInvalidSecretKey byteString))
+
+    Test.it test "succeeds with valid input" $ do
+      let result = SecretKey.parse $ Ascii.pack "whsk_QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU="
+      secretKey <- fmap SecretKey.MkSecretKey . Error.throwCryptoErrorIO . Ed25519.secretKey $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"
+      Test.assertEq test result (Right secretKey)
+
+  Test.describe test "render" $ do
+    Test.it test "works" $ do
+      secretKey <- fmap SecretKey.MkSecretKey . Error.throwCryptoErrorIO . Ed25519.secretKey $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"
+      Test.assertEq test (SecretKey.render secretKey) (Ascii.pack "whsk_QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU=")
diff --git a/source/library/Signet/Unstable/Type/SecretTest.hs b/source/library/Signet/Unstable/Type/SecretTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/SecretTest.hs
@@ -0,0 +1,30 @@
+module Signet.Unstable.Type.SecretTest where
+
+import qualified Data.ByteArray as ByteArray
+import qualified Data.ByteString.Char8 as Ascii
+import qualified Signet.Unstable.Exception.InvalidSecret as InvalidSecret
+import qualified Signet.Unstable.Type.Secret as Secret
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Type.Secret" $ do
+  Test.describe test "parse" $ do
+    Test.it test "fails with invalid prefix" $ do
+      let byteString = Ascii.pack "invalid"
+      let result = Secret.parse byteString
+      Test.assertEq test result (Left (InvalidSecret.MkInvalidSecret byteString))
+
+    Test.it test "fails with invalid input" $ do
+      let byteString = Ascii.pack "whsec_invalid"
+      let result = Secret.parse byteString
+      Test.assertEq test result (Left (InvalidSecret.MkInvalidSecret byteString))
+
+    Test.it test "succeeds with valid input" $ do
+      let result = Secret.parse $ Ascii.pack "whsec_MDEyMzQ1Njc4OQ=="
+      let secret = Secret.MkSecret . ByteArray.convert $ Ascii.pack "0123456789"
+      Test.assertEq test result (Right secret)
+
+  Test.describe test "render" $ do
+    Test.it test "works" $ do
+      let secret = Secret.MkSecret . ByteArray.convert $ Ascii.pack "0123456789"
+      Test.assertEq test (Secret.render secret) (Ascii.pack "whsec_MDEyMzQ1Njc4OQ==")
diff --git a/source/library/Signet/Unstable/Type/Signature.hs b/source/library/Signet/Unstable/Type/Signature.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/Signature.hs
@@ -0,0 +1,49 @@
+module Signet.Unstable.Type.Signature where
+
+import qualified Data.Bifunctor as Bifunctor
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Char8 as Ascii
+import qualified Data.Word as Word
+import qualified Signet.Unstable.Exception.InvalidSignature as InvalidSignature
+import qualified Signet.Unstable.Type.AsymmetricSignature as AsymmetricSignature
+import qualified Signet.Unstable.Type.SymmetricSignature as SymmetricSignature
+import qualified Signet.Unstable.Type.UnknownSignature as UnknownSignature
+
+data Signature
+  = Asymmetric AsymmetricSignature.AsymmetricSignature
+  | Symmetric SymmetricSignature.SymmetricSignature
+  deriving (Eq, Show)
+
+separator :: Word.Word8
+separator = 0x2c
+
+asymmetricPrefix :: ByteString.ByteString
+asymmetricPrefix = Ascii.pack "v1a"
+
+symmetricPrefix :: ByteString.ByteString
+symmetricPrefix = Ascii.pack "v1"
+
+parse :: ByteString.ByteString -> Either InvalidSignature.InvalidSignature (Either UnknownSignature.UnknownSignature Signature)
+parse prefixed = do
+  let (prefix, rest) = ByteString.break (== separator) prefixed
+  case ByteString.drop 1 rest of
+    byteString
+      | prefix == asymmetricPrefix ->
+          Bifunctor.bimap InvalidSignature.InvalidAsymmetricSignature (Right . Asymmetric) $
+            AsymmetricSignature.parse byteString
+      | prefix == symmetricPrefix ->
+          Bifunctor.bimap InvalidSignature.InvalidSymmetricSignature (Right . Symmetric) $
+            SymmetricSignature.parse byteString
+    _ -> Right . Left $ UnknownSignature.MkUnknownSignature prefixed
+
+render :: Signature -> ByteString.ByteString
+render signature =
+  case signature of
+    Asymmetric asymmetricSignature ->
+      asymmetricPrefix
+        <> ByteString.singleton separator
+        <> AsymmetricSignature.render asymmetricSignature
+    Symmetric symmetricSignature ->
+      symmetricPrefix
+        <> ByteString.singleton separator
+        <> SymmetricSignature.render symmetricSignature
diff --git a/source/library/Signet/Unstable/Type/SignatureTest.hs b/source/library/Signet/Unstable/Type/SignatureTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/SignatureTest.hs
@@ -0,0 +1,52 @@
+module Signet.Unstable.Type.SignatureTest where
+
+import qualified Crypto.Error as Error
+import qualified Crypto.Hash as Hash
+import qualified Crypto.PubKey.Ed25519 as Ed25519
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Char8 as Ascii
+import qualified Signet.Unstable.Exception.InvalidAsymmetricSignature as InvalidAsymmetricSignature
+import qualified Signet.Unstable.Exception.InvalidSignature as InvalidSignature
+import qualified Signet.Unstable.Exception.InvalidSymmetricSignature as InvalidSymmetricSignature
+import qualified Signet.Unstable.Type.AsymmetricSignature as AsymmetricSignature
+import qualified Signet.Unstable.Type.Signature as Signature
+import qualified Signet.Unstable.Type.SymmetricSignature as SymmetricSignature
+import qualified Signet.Unstable.Type.Test as Test
+import qualified Signet.Unstable.Type.UnknownSignature as UnknownSignature
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Type.Signature" $ do
+  Test.describe test "parse" $ do
+    Test.it test "fails with invalid asymmetric signature" $ do
+      let byteString = Ascii.pack "invalid"
+      let result = Signature.parse $ Ascii.pack "v1a," <> byteString
+      Test.assertEq test result (Left (InvalidSignature.InvalidAsymmetricSignature $ InvalidAsymmetricSignature.MkInvalidAsymmetricSignature byteString))
+
+    Test.it test "fails with invalid symmetric signature" $ do
+      let byteString = Ascii.pack "invalid"
+      let result = Signature.parse $ Ascii.pack "v1," <> byteString
+      Test.assertEq test result (Left (InvalidSignature.InvalidSymmetricSignature $ InvalidSymmetricSignature.MkInvalidSymmetricSignature byteString))
+
+    Test.it test "returns unknown signature with unrecognized prefix" $ do
+      let byteString = Ascii.pack "unknown"
+      let result = Signature.parse byteString
+      Test.assertEq test result (Right (Left $ UnknownSignature.MkUnknownSignature byteString))
+
+    Test.it test "succeeds with valid asymmetric signature" $ do
+      let byteString = Ascii.pack "v1a,QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVotMDEyMzQ1Njc4OS1hYmNkZWZnaGlqa2xtbm9wcXJzdHV2cXh5eg=="
+      signature <- fmap (Signature.Asymmetric . AsymmetricSignature.MkAsymmetricSignature) . Error.throwCryptoErrorIO . Ed25519.signature $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789-abcdefghijklmnopqrstuvqxyz"
+      Test.assertEq test (Signature.parse byteString) (Right (Right signature))
+
+    Test.it test "succeeds with valid symmetric signature" $ do
+      let byteString = Ascii.pack "v1,47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
+      let signature = Signature.Symmetric . SymmetricSignature.MkSymmetricSignature $ Hash.hash ByteString.empty
+      Test.assertEq test (Signature.parse byteString) (Right (Right signature))
+
+  Test.describe test "render" $ do
+    Test.it test "renders asymmetric signature correctly" $ do
+      signature <- fmap (Signature.Asymmetric . AsymmetricSignature.MkAsymmetricSignature) . Error.throwCryptoErrorIO . Ed25519.signature $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789-abcdefghijklmnopqrstuvqxyz"
+      Test.assertEq test (Signature.render signature) (Ascii.pack "v1a,QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVotMDEyMzQ1Njc4OS1hYmNkZWZnaGlqa2xtbm9wcXJzdHV2cXh5eg==")
+
+    Test.it test "renders symmetric signature correctly" $ do
+      let signature = Signature.Symmetric . SymmetricSignature.MkSymmetricSignature $ Hash.hash ByteString.empty
+      Test.assertEq test (Signature.render signature) (Ascii.pack "v1,47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=")
diff --git a/source/library/Signet/Unstable/Type/Signatures.hs b/source/library/Signet/Unstable/Type/Signatures.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/Signatures.hs
@@ -0,0 +1,30 @@
+module Signet.Unstable.Type.Signatures where
+
+import qualified Data.ByteString as ByteString
+import qualified Data.Either as Either
+import qualified Data.Word as Word
+import qualified Signet.Unstable.Exception.InvalidSignature as InvalidSignature
+import qualified Signet.Unstable.Type.Signature as Signature
+import qualified Signet.Unstable.Type.UnknownSignature as UnknownSignature
+
+newtype Signatures
+  = MkSignatures [Signature.Signature]
+  deriving (Eq, Show)
+
+unwrap :: Signatures -> [Signature.Signature]
+unwrap (MkSignatures signatures) = signatures
+
+separator :: Word.Word8
+separator = 0x20
+
+parse :: ByteString.ByteString -> Either InvalidSignature.InvalidSignature ([UnknownSignature.UnknownSignature], Signatures)
+parse =
+  fmap (fmap MkSignatures . Either.partitionEithers)
+    . traverse Signature.parse
+    . ByteString.split separator
+
+render :: Signatures -> ByteString.ByteString
+render =
+  ByteString.intercalate (ByteString.singleton separator)
+    . fmap Signature.render
+    . unwrap
diff --git a/source/library/Signet/Unstable/Type/SignaturesTest.hs b/source/library/Signet/Unstable/Type/SignaturesTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/SignaturesTest.hs
@@ -0,0 +1,71 @@
+module Signet.Unstable.Type.SignaturesTest where
+
+import qualified Crypto.Error as Error
+import qualified Crypto.Hash as Hash
+import qualified Crypto.PubKey.Ed25519 as Ed25519
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Char8 as Ascii
+import qualified Signet.Unstable.Exception.InvalidAsymmetricSignature as InvalidAsymmetricSignature
+import qualified Signet.Unstable.Exception.InvalidSignature as InvalidSignature
+import qualified Signet.Unstable.Exception.InvalidSymmetricSignature as InvalidSymmetricSignature
+import qualified Signet.Unstable.Type.AsymmetricSignature as AsymmetricSignature
+import qualified Signet.Unstable.Type.Signature as Signature
+import qualified Signet.Unstable.Type.Signatures as Signatures
+import qualified Signet.Unstable.Type.SymmetricSignature as SymmetricSignature
+import qualified Signet.Unstable.Type.Test as Test
+import qualified Signet.Unstable.Type.UnknownSignature as UnknownSignature
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Type.Signatures" $ do
+  Test.describe test "parse" $ do
+    Test.it test "succeeds with no signatures" $ do
+      Test.assertEq test (Signatures.parse ByteString.empty) (Right ([], Signatures.MkSignatures []))
+
+    Test.it test "succeeds with one signature" $ do
+      let signature = Signature.Symmetric . SymmetricSignature.MkSymmetricSignature $ Hash.hash ByteString.empty
+      let byteString = Ascii.pack "v1,47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
+      Test.assertEq test (Signatures.parse byteString) (Right ([], Signatures.MkSignatures [signature]))
+
+    Test.it test "succeeds with many signatures" $ do
+      let symmetricSignature = SymmetricSignature.MkSymmetricSignature $ Hash.hash ByteString.empty
+      asymmetricSignature <- fmap AsymmetricSignature.MkAsymmetricSignature . Error.throwCryptoErrorIO . Ed25519.signature $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789-abcdefghijklmnopqrstuvqxyz"
+      let byteString = Ascii.pack "v1,47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU= v1a,QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVotMDEyMzQ1Njc4OS1hYmNkZWZnaGlqa2xtbm9wcXJzdHV2cXh5eg=="
+      let signatures =
+            Signatures.MkSignatures
+              [ Signature.Symmetric symmetricSignature,
+                Signature.Asymmetric asymmetricSignature
+              ]
+      Test.assertEq test (Signatures.parse byteString) (Right ([], signatures))
+
+    Test.it test "fails with an invalid symmetric signature" $ do
+      let x = Ascii.pack "invalid"
+      let byteString = Ascii.pack "v1," <> x
+      Test.assertEq test (Signatures.parse byteString) (Left (InvalidSignature.InvalidSymmetricSignature $ InvalidSymmetricSignature.MkInvalidSymmetricSignature x))
+
+    Test.it test "fails with an invalid asymmetric signature" $ do
+      let x = Ascii.pack "invalid"
+      let byteString = Ascii.pack "v1a," <> x
+      Test.assertEq test (Signatures.parse byteString) (Left (InvalidSignature.InvalidAsymmetricSignature $ InvalidAsymmetricSignature.MkInvalidAsymmetricSignature x))
+
+    Test.it test "succeeds with an unknown signature" $ do
+      let byteString = Ascii.pack "unknown"
+      Test.assertEq test (Signatures.parse byteString) (Right ([UnknownSignature.MkUnknownSignature byteString], Signatures.MkSignatures []))
+
+  Test.describe test "render" $ do
+    Test.it test "renders no signatures" $ do
+      let signatures = Signatures.MkSignatures []
+      Test.assertEq test (Signatures.render signatures) ByteString.empty
+
+    Test.it test "renders one signature" $ do
+      let signatures = Signatures.MkSignatures . pure . Signature.Symmetric . SymmetricSignature.MkSymmetricSignature $ Hash.hash ByteString.empty
+      Test.assertEq test (Signatures.render signatures) (Ascii.pack "v1,47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=")
+
+    Test.it test "renders many signatures" $ do
+      let symmetricSignature = SymmetricSignature.MkSymmetricSignature $ Hash.hash ByteString.empty
+      asymmetricSignature <- fmap AsymmetricSignature.MkAsymmetricSignature . Error.throwCryptoErrorIO . Ed25519.signature $ Ascii.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789-abcdefghijklmnopqrstuvqxyz"
+      let signatures =
+            Signatures.MkSignatures
+              [ Signature.Symmetric symmetricSignature,
+                Signature.Asymmetric asymmetricSignature
+              ]
+      Test.assertEq test (Signatures.render signatures) (Ascii.pack "v1,47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU= v1a,QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVotMDEyMzQ1Njc4OS1hYmNkZWZnaGlqa2xtbm9wcXJzdHV2cXh5eg==")
diff --git a/source/library/Signet/Unstable/Type/Signer.hs b/source/library/Signet/Unstable/Type/Signer.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/Signer.hs
@@ -0,0 +1,49 @@
+module Signet.Unstable.Type.Signer where
+
+import qualified Crypto.MAC.HMAC as Hmac
+import qualified Crypto.PubKey.Ed25519 as Ed25519
+import qualified Data.ByteString as ByteString
+import qualified Signet.Unstable.Exception.InvalidSigner as InvalidSigner
+import qualified Signet.Unstable.Type.AsymmetricSignature as AsymmetricSignature
+import qualified Signet.Unstable.Type.Message as Message
+import qualified Signet.Unstable.Type.Secret as Secret
+import qualified Signet.Unstable.Type.SecretKey as SecretKey
+import qualified Signet.Unstable.Type.Signature as Signature
+import qualified Signet.Unstable.Type.SymmetricSignature as SymmetricSignature
+
+data Signer
+  = Asymmetric SecretKey.SecretKey
+  | Symmetric Secret.Secret
+  deriving (Eq, Show)
+
+parse :: ByteString.ByteString -> Either InvalidSigner.InvalidSigner Signer
+parse byteString = case SecretKey.parse byteString of
+  Right secretKey -> Right $ Asymmetric secretKey
+  Left _ -> case Secret.parse byteString of
+    Right secret -> Right $ Symmetric secret
+    Left _ -> Left $ InvalidSigner.MkInvalidSigner byteString
+
+render :: Signer -> ByteString.ByteString
+render signer = case signer of
+  Asymmetric secretKey -> SecretKey.render secretKey
+  Symmetric secret -> Secret.render secret
+
+sign :: Signer -> Message.Message -> Signature.Signature
+sign signer = case signer of
+  Asymmetric secretKey -> Signature.Asymmetric . asymmetric secretKey
+  Symmetric secret -> Signature.Symmetric . symmetric secret
+
+asymmetric :: SecretKey.SecretKey -> Message.Message -> AsymmetricSignature.AsymmetricSignature
+asymmetric secretKey =
+  let sk = SecretKey.unwrap secretKey
+      pk = Ed25519.toPublic sk
+   in AsymmetricSignature.MkAsymmetricSignature
+        . Ed25519.sign sk pk
+        . Message.render
+
+symmetric :: Secret.Secret -> Message.Message -> SymmetricSignature.SymmetricSignature
+symmetric secret =
+  SymmetricSignature.MkSymmetricSignature
+    . Hmac.hmacGetDigest
+    . Hmac.hmac (Secret.unwrap secret)
+    . Message.render
diff --git a/source/library/Signet/Unstable/Type/SignerTest.hs b/source/library/Signet/Unstable/Type/SignerTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/SignerTest.hs
@@ -0,0 +1,75 @@
+module Signet.Unstable.Type.SignerTest where
+
+import qualified Data.ByteString.Char8 as Ascii
+import qualified Signet.Unstable.Exception.InvalidSigner as InvalidSigner
+import qualified Signet.Unstable.Extra.Either as Either
+import qualified Signet.Unstable.Type.AsymmetricSignature as AsymmetricSignature
+import qualified Signet.Unstable.Type.Message as Message
+import qualified Signet.Unstable.Type.Secret as Secret
+import qualified Signet.Unstable.Type.SecretKey as SecretKey
+import qualified Signet.Unstable.Type.Signature as Signature
+import qualified Signet.Unstable.Type.Signer as Signer
+import qualified Signet.Unstable.Type.SymmetricSignature as SymmetricSignature
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Type.Signer" $ do
+  Test.describe test "parse" $ do
+    Test.it test "succeeds with a valid secret key" $ do
+      let byteString = Ascii.pack "whsk_QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU="
+      let actual = Signer.parse byteString
+      secretKey <- Either.throw $ SecretKey.parse byteString
+      Test.assertEq test actual (Right (Signer.Asymmetric secretKey))
+
+    Test.it test "succeeds with a valid secret" $ do
+      let byteString = Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
+      let actual = Signer.parse byteString
+      secret <- Either.throw $ Secret.parse byteString
+      Test.assertEq test actual (Right (Signer.Symmetric secret))
+
+    Test.it test "fails with invalid input" $ do
+      let byteString = Ascii.pack "invalid"
+      let actual = Signer.parse byteString
+      Test.assertEq test actual (Left (InvalidSigner.MkInvalidSigner byteString))
+
+  Test.describe test "render" $ do
+    Test.it test "works with a secret key" $ do
+      let byteString = Ascii.pack "whsk_QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU="
+      secretKey <- Either.throw . SecretKey.parse $ byteString
+      Test.assertEq test (Signer.render (Signer.Asymmetric secretKey)) byteString
+
+    Test.it test "works with a secret" $ do
+      let byteString = Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
+      secret <- Either.throw . Secret.parse $ byteString
+      Test.assertEq test (Signer.render (Signer.Symmetric secret)) byteString
+
+  Test.describe test "sign" $ do
+    Test.it test "works with asymmetric" $ do
+      signer <- Either.throw . Signer.parse $ Ascii.pack "whsk_QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU="
+      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
+      let actual = Signer.sign signer message
+      Right expected <- Either.throw . Signature.parse $ Ascii.pack "v1a,CV1O+PvrwXM42OMUX+tmm6bA3cS0tgLp0qo3YKuu0MGmBrsUhA0MHXF11HsEUJtPfTKs80WE7WUKVt9TueLDCQ=="
+      Test.assertEq test actual expected
+
+    Test.it test "works with symmetric" $ do
+      signer <- Either.throw . Signer.parse $ Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
+      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
+      let actual = Signer.sign signer message
+      Right expected <- Either.throw . Signature.parse $ Ascii.pack "v1,IywpE5NXy+JdAScgR7j5Pt59GjmazD7iJuVsQoRZFyw="
+      Test.assertEq test actual expected
+
+  Test.describe test "asymmetric" $ do
+    Test.it test "creates correct asymmetric signature" $ do
+      secretKey <- Either.throw . SecretKey.parse $ Ascii.pack "whsk_QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU="
+      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
+      let actual = Signer.asymmetric secretKey message
+      expected <- Either.throw . AsymmetricSignature.parse $ Ascii.pack "CV1O+PvrwXM42OMUX+tmm6bA3cS0tgLp0qo3YKuu0MGmBrsUhA0MHXF11HsEUJtPfTKs80WE7WUKVt9TueLDCQ=="
+      Test.assertEq test actual expected
+
+  Test.describe test "symmetric" $ do
+    Test.it test "creates correct symmetric signature" $ do
+      secret <- Either.throw . Secret.parse $ Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
+      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
+      let actual = Signer.symmetric secret message
+      expected <- Either.throw . SymmetricSignature.parse $ Ascii.pack "IywpE5NXy+JdAScgR7j5Pt59GjmazD7iJuVsQoRZFyw="
+      Test.assertEq test actual expected
diff --git a/source/library/Signet/Unstable/Type/SymmetricSignature.hs b/source/library/Signet/Unstable/Type/SymmetricSignature.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/SymmetricSignature.hs
@@ -0,0 +1,28 @@
+module Signet.Unstable.Type.SymmetricSignature where
+
+import qualified Crypto.Hash as Hash
+import qualified Data.ByteArray as ByteArray
+import qualified Data.ByteArray.Encoding as Encoding
+import qualified Data.ByteString as ByteString
+import qualified Data.Function as Function
+import qualified Signet.Unstable.Exception.InvalidSymmetricSignature as InvalidSymmetricSignature
+import qualified Signet.Unstable.Extra.Either as Either
+import qualified Signet.Unstable.Extra.Maybe as Maybe
+
+newtype SymmetricSignature
+  = MkSymmetricSignature (Hash.Digest Hash.SHA256)
+  deriving (Show)
+
+instance Eq SymmetricSignature where
+  (==) = Function.on ByteArray.constEq unwrap
+
+unwrap :: SymmetricSignature -> Hash.Digest Hash.SHA256
+unwrap (MkSymmetricSignature digest) = digest
+
+parse :: ByteString.ByteString -> Either InvalidSymmetricSignature.InvalidSymmetricSignature SymmetricSignature
+parse encoded = Maybe.note (InvalidSymmetricSignature.MkInvalidSymmetricSignature encoded) $ do
+  byteString <- Either.hush $ Encoding.convertFromBase Encoding.Base64 encoded
+  MkSymmetricSignature <$> Hash.digestFromByteString (byteString :: ByteString.ByteString)
+
+render :: SymmetricSignature -> ByteString.ByteString
+render = Encoding.convertToBase Encoding.Base64 . unwrap
diff --git a/source/library/Signet/Unstable/Type/SymmetricSignatureTest.hs b/source/library/Signet/Unstable/Type/SymmetricSignatureTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/SymmetricSignatureTest.hs
@@ -0,0 +1,26 @@
+module Signet.Unstable.Type.SymmetricSignatureTest where
+
+import qualified Crypto.Hash as Hash
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Char8 as Ascii
+import qualified Signet.Unstable.Exception.InvalidSymmetricSignature as InvalidSymmetricSignature
+import qualified Signet.Unstable.Type.SymmetricSignature as SymmetricSignature
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Type.SymmetricSignature" $ do
+  Test.describe test "parse" $ do
+    Test.it test "fails with invalid symmetric signature" $ do
+      let byteString = Ascii.pack "invalid"
+      let result = SymmetricSignature.parse byteString
+      Test.assertEq test result (Left (InvalidSymmetricSignature.MkInvalidSymmetricSignature byteString))
+
+    Test.it test "succeeds with valid symmetric signature" $ do
+      let byteString = Ascii.pack "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
+      let signature = SymmetricSignature.MkSymmetricSignature $ Hash.hash ByteString.empty
+      Test.assertEq test (SymmetricSignature.parse byteString) (Right signature)
+
+  Test.describe test "render" $ do
+    Test.it test "renders symmetric signature correctly" $ do
+      let signature = SymmetricSignature.MkSymmetricSignature $ Hash.hash ByteString.empty
+      Test.assertEq test (SymmetricSignature.render signature) (Ascii.pack "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=")
diff --git a/source/library/Signet/Unstable/Type/Test.hs b/source/library/Signet/Unstable/Type/Test.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/Test.hs
@@ -0,0 +1,20 @@
+{-# LANGUAGE RankNTypes #-}
+
+module Signet.Unstable.Type.Test where
+
+import qualified Control.Monad as Monad
+import qualified Data.Void as Void
+import qualified GHC.Stack as Stack
+
+data Test spec = MkTest
+  { assertFailure :: (Stack.HasCallStack) => String -> IO Void.Void,
+    describe :: String -> spec () -> spec (),
+    it :: String -> IO () -> spec ()
+  }
+
+assertEq :: (Stack.HasCallStack, Eq a, Show a) => Test tree -> a -> a -> IO ()
+assertEq test expected actual =
+  Monad.when (expected /= actual)
+    . Monad.void
+    . assertFailure test
+    $ "expected " <> show expected <> " but got " <> show actual
diff --git a/source/library/Signet/Unstable/Type/Timestamp.hs b/source/library/Signet/Unstable/Type/Timestamp.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/Timestamp.hs
@@ -0,0 +1,33 @@
+module Signet.Unstable.Type.Timestamp where
+
+import qualified Data.ByteString as ByteString
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Text
+import qualified Data.Time as Time
+import qualified Signet.Unstable.Exception.InvalidTimestamp as InvalidTimestamp
+import qualified Signet.Unstable.Extra.Either as Either
+import qualified Signet.Unstable.Extra.Maybe as Maybe
+
+newtype Timestamp
+  = MkTimestamp Time.UTCTime
+  deriving (Eq, Show)
+
+unwrap :: Timestamp -> Time.UTCTime
+unwrap (MkTimestamp utcTime) = utcTime
+
+format :: String
+format = "%s"
+
+parse :: ByteString.ByteString -> Either InvalidTimestamp.InvalidTimestamp Timestamp
+parse byteString = Maybe.note (InvalidTimestamp.MkInvalidTimestamp byteString) $ do
+  text <- Either.hush $ Text.decodeUtf8' byteString
+  fmap MkTimestamp
+    . Time.parseTimeM False Time.defaultTimeLocale format
+    $ Text.unpack text
+
+render :: Timestamp -> ByteString.ByteString
+render =
+  Text.encodeUtf8
+    . Text.pack
+    . Time.formatTime Time.defaultTimeLocale format
+    . unwrap
diff --git a/source/library/Signet/Unstable/Type/TimestampTest.hs b/source/library/Signet/Unstable/Type/TimestampTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/TimestampTest.hs
@@ -0,0 +1,32 @@
+module Signet.Unstable.Type.TimestampTest where
+
+import qualified Data.ByteString.Char8 as Ascii
+import qualified Data.Time as Time
+import qualified Signet.Unstable.Exception.InvalidTimestamp as InvalidTimestamp
+import qualified Signet.Unstable.Type.Test as Test
+import qualified Signet.Unstable.Type.Timestamp as Timestamp
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Type.Timestamp" $ do
+  Test.describe test "parse" $ do
+    Test.it test "fails with invalid timestamp format" $ do
+      let byteString = Ascii.pack "invalid-timestamp"
+      let result = Timestamp.parse byteString
+      Test.assertEq test result (Left (InvalidTimestamp.MkInvalidTimestamp byteString))
+
+    Test.it test "succeeds with valid timestamp format" $ do
+      let byteString = Ascii.pack "1617235200"
+      let result = Timestamp.parse byteString
+      case result of
+        Right timestamp -> do
+          let utcTime = Timestamp.unwrap timestamp
+          let expectedTime = Time.UTCTime (Time.fromGregorian 2021 4 1) 0
+          Test.assertEq test utcTime expectedTime
+        Left _ -> fail "Expected Right but got Left"
+
+  Test.describe test "render" $ do
+    Test.it test "returns the correct ByteString representation" $ do
+      let utcTime = Time.UTCTime (Time.fromGregorian 2021 4 1) 0
+      let timestamp = Timestamp.MkTimestamp utcTime
+      let expected = Ascii.pack "1617235200"
+      Test.assertEq test (Timestamp.render timestamp) expected
diff --git a/source/library/Signet/Unstable/Type/Tolerance.hs b/source/library/Signet/Unstable/Type/Tolerance.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/Tolerance.hs
@@ -0,0 +1,29 @@
+module Signet.Unstable.Type.Tolerance where
+
+import qualified Control.Monad as Monad
+import qualified Data.Time as Time
+import qualified Signet.Unstable.Exception.ToleranceException as ToleranceException
+import qualified Signet.Unstable.Type.Timestamp as Timestamp
+
+newtype Tolerance
+  = MkTolerance Time.NominalDiffTime
+  deriving (Eq, Show)
+
+unwrap :: Tolerance -> Time.NominalDiffTime
+unwrap (MkTolerance nominalDiffTime) = nominalDiffTime
+
+typical :: Tolerance
+typical = MkTolerance 300
+
+check ::
+  Tolerance ->
+  Time.UTCTime ->
+  Timestamp.Timestamp ->
+  Either ToleranceException.ToleranceException ()
+check tolerance utcTime timestamp = do
+  let diff = Time.diffUTCTime utcTime $ Timestamp.unwrap timestamp
+  let hi = unwrap tolerance
+  let lo = negate hi
+  Monad.when (lo > diff || diff > hi)
+    . Left
+    $ ToleranceException.MkToleranceException timestamp
diff --git a/source/library/Signet/Unstable/Type/ToleranceTest.hs b/source/library/Signet/Unstable/Type/ToleranceTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/ToleranceTest.hs
@@ -0,0 +1,33 @@
+module Signet.Unstable.Type.ToleranceTest where
+
+import qualified Data.Time as Time
+import qualified Signet.Unstable.Exception.ToleranceException as ToleranceException
+import qualified Signet.Unstable.Type.Test as Test
+import qualified Signet.Unstable.Type.Timestamp as Timestamp
+import qualified Signet.Unstable.Type.Tolerance as Tolerance
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Type.Tolerance" $ do
+  Test.describe test "check" $ do
+    Test.it test "succeeds when timestamp is within tolerance" $ do
+      let tolerance = Tolerance.MkTolerance 1
+      let utcTime = Time.UTCTime (Time.fromGregorian 2001 1 1) 60
+      let timestamp = Timestamp.MkTimestamp utcTime
+      let result = Tolerance.check tolerance utcTime timestamp
+      Test.assertEq test result (Right ())
+
+    Test.it test "fails when timestamp is too old" $ do
+      let tolerance = Tolerance.MkTolerance 1
+      let utcTime = Time.UTCTime (Time.fromGregorian 2001 1 1) 60
+      let timestamp = Timestamp.MkTimestamp utcTime
+      let now = Time.addUTCTime 2 utcTime
+      let result = Tolerance.check tolerance now timestamp
+      Test.assertEq test result (Left (ToleranceException.MkToleranceException timestamp))
+
+    Test.it test "fails when timestamp is in the future" $ do
+      let tolerance = Tolerance.MkTolerance 1
+      let utcTime = Time.UTCTime (Time.fromGregorian 2001 1 1) 60
+      let timestamp = Timestamp.MkTimestamp utcTime
+      let now = Time.addUTCTime (-2) utcTime
+      let result = Tolerance.check tolerance now timestamp
+      Test.assertEq test result (Left (ToleranceException.MkToleranceException timestamp))
diff --git a/source/library/Signet/Unstable/Type/UnknownSignature.hs b/source/library/Signet/Unstable/Type/UnknownSignature.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/UnknownSignature.hs
@@ -0,0 +1,10 @@
+module Signet.Unstable.Type.UnknownSignature where
+
+import qualified Data.ByteString as ByteString
+
+newtype UnknownSignature
+  = MkUnknownSignature ByteString.ByteString
+  deriving (Eq, Show)
+
+unwrap :: UnknownSignature -> ByteString.ByteString
+unwrap (MkUnknownSignature byteString) = byteString
diff --git a/source/library/Signet/Unstable/Type/UnknownSignatureTest.hs b/source/library/Signet/Unstable/Type/UnknownSignatureTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/UnknownSignatureTest.hs
@@ -0,0 +1,7 @@
+module Signet.Unstable.Type.UnknownSignatureTest where
+
+import qualified Signet.Unstable.Type.Test as Test
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Type.UnknownSignature" $ do
+  pure ()
diff --git a/source/library/Signet/Unstable/Type/Verifier.hs b/source/library/Signet/Unstable/Type/Verifier.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/Verifier.hs
@@ -0,0 +1,75 @@
+module Signet.Unstable.Type.Verifier where
+
+import qualified Crypto.PubKey.Ed25519 as Ed25519
+import qualified Data.ByteString as ByteString
+import qualified Data.List as List
+import qualified Data.Maybe as Maybe
+import qualified Signet.Unstable.Exception.InvalidVerifier as InvalidVerifier
+import qualified Signet.Unstable.Exception.VerificationException as VerificationException
+import qualified Signet.Unstable.Extra.Maybe as Maybe
+import qualified Signet.Unstable.Type.AsymmetricSignature as AsymmetricSignature
+import qualified Signet.Unstable.Type.Message as Message
+import qualified Signet.Unstable.Type.PublicKey as PublicKey
+import qualified Signet.Unstable.Type.Secret as Secret
+import qualified Signet.Unstable.Type.Signature as Signature
+import qualified Signet.Unstable.Type.Signatures as Signatures
+import qualified Signet.Unstable.Type.Signer as Signer
+import qualified Signet.Unstable.Type.SymmetricSignature as SymmetricSignature
+
+data Verifier
+  = Asymmetric PublicKey.PublicKey
+  | Symmetric Secret.Secret
+  deriving (Eq, Show)
+
+parse :: ByteString.ByteString -> Either InvalidVerifier.InvalidVerifier Verifier
+parse byteString = case PublicKey.parse byteString of
+  Right publicKey -> Right $ Asymmetric publicKey
+  Left _ -> case Secret.parse byteString of
+    Right secret -> Right $ Symmetric secret
+    Left _ -> Left $ InvalidVerifier.MkInvalidVerifier byteString
+
+render :: Verifier -> ByteString.ByteString
+render verifier = case verifier of
+  Asymmetric publicKey -> PublicKey.render publicKey
+  Symmetric secret -> Secret.render secret
+
+verify ::
+  Verifier ->
+  Message.Message ->
+  Signatures.Signatures ->
+  Either VerificationException.VerificationException Signature.Signature
+verify verifier message =
+  case verifier of
+    Asymmetric publicKey -> fmap Signature.Asymmetric . asymmetric publicKey message
+    Symmetric secret -> fmap Signature.Symmetric . symmetric secret message
+
+asymmetric ::
+  PublicKey.PublicKey ->
+  Message.Message ->
+  Signatures.Signatures ->
+  Either VerificationException.VerificationException AsymmetricSignature.AsymmetricSignature
+asymmetric publicKey message =
+  let toAsymmetric signature = case signature of
+        Signature.Asymmetric asymmetricSignature -> Just asymmetricSignature
+        Signature.Symmetric _ -> Nothing
+      pk = PublicKey.unwrap publicKey
+      bs = Message.render message
+   in Maybe.note (VerificationException.MkVerificationException $ Message.id_ message)
+        . List.find (Ed25519.verify pk bs . AsymmetricSignature.unwrap)
+        . Maybe.mapMaybe toAsymmetric
+        . Signatures.unwrap
+
+symmetric ::
+  Secret.Secret ->
+  Message.Message ->
+  Signatures.Signatures ->
+  Either VerificationException.VerificationException SymmetricSignature.SymmetricSignature
+symmetric secret message =
+  let toSymmetric signature = case signature of
+        Signature.Asymmetric _ -> Nothing
+        Signature.Symmetric symmetricSignature -> Just symmetricSignature
+      expected = Signer.symmetric secret message
+   in Maybe.note (VerificationException.MkVerificationException $ Message.id_ message)
+        . List.find (expected ==)
+        . Maybe.mapMaybe toSymmetric
+        . Signatures.unwrap
diff --git a/source/library/Signet/Unstable/Type/VerifierTest.hs b/source/library/Signet/Unstable/Type/VerifierTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/Unstable/Type/VerifierTest.hs
@@ -0,0 +1,109 @@
+module Signet.Unstable.Type.VerifierTest where
+
+import qualified Data.ByteString.Char8 as Ascii
+import qualified Signet.Unstable.Exception.InvalidVerifier as InvalidVerifier
+import qualified Signet.Unstable.Exception.VerificationException as VerificationException
+import qualified Signet.Unstable.Extra.Either as Either
+import qualified Signet.Unstable.Type.AsymmetricSignature as AsymmetricSignature
+import qualified Signet.Unstable.Type.Message as Message
+import qualified Signet.Unstable.Type.PublicKey as PublicKey
+import qualified Signet.Unstable.Type.Secret as Secret
+import qualified Signet.Unstable.Type.Signature as Signature
+import qualified Signet.Unstable.Type.Signatures as Signatures
+import qualified Signet.Unstable.Type.SymmetricSignature as SymmetricSignature
+import qualified Signet.Unstable.Type.Test as Test
+import qualified Signet.Unstable.Type.Verifier as Verifier
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable.Type.Verifier" $ do
+  Test.describe test "parse" $ do
+    Test.it test "succeeds with a valid public key" $ do
+      let byteString = Ascii.pack "whpk_wuzPrKxPfWpJSsXgyg/MEoMGvjs5SjDO4ad6X4ZYqqg="
+      let actual = Verifier.parse byteString
+      publicKey <- Either.throw $ PublicKey.parse byteString
+      Test.assertEq test actual (Right (Verifier.Asymmetric publicKey))
+
+    Test.it test "succeeds with a valid secret" $ do
+      let byteString = Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
+      let actual = Verifier.parse byteString
+      secret <- Either.throw $ Secret.parse byteString
+      Test.assertEq test actual (Right (Verifier.Symmetric secret))
+
+    Test.it test "fails with invalid input" $ do
+      let byteString = Ascii.pack "invalid"
+      let actual = Verifier.parse byteString
+      Test.assertEq test actual (Left (InvalidVerifier.MkInvalidVerifier byteString))
+
+  Test.describe test "render" $ do
+    Test.it test "works with a public key" $ do
+      let byteString = Ascii.pack "whpk_wuzPrKxPfWpJSsXgyg/MEoMGvjs5SjDO4ad6X4ZYqqg="
+      publicKey <- Either.throw . PublicKey.parse $ byteString
+      Test.assertEq test (Verifier.render (Verifier.Asymmetric publicKey)) byteString
+
+    Test.it test "works with a secret" $ do
+      let byteString = Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
+      secret <- Either.throw . Secret.parse $ byteString
+      Test.assertEq test (Verifier.render (Verifier.Symmetric secret)) byteString
+
+  Test.describe test "verify" $ do
+    Test.it test "succeeds with a valid asymmetric signature" $ do
+      verifier <- Either.throw . Verifier.parse $ Ascii.pack "whpk_wuzPrKxPfWpJSsXgyg/MEoMGvjs5SjDO4ad6X4ZYqqg="
+      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
+      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1a,CV1O+PvrwXM42OMUX+tmm6bA3cS0tgLp0qo3YKuu0MGmBrsUhA0MHXF11HsEUJtPfTKs80WE7WUKVt9TueLDCQ=="
+      let actual = Verifier.verify verifier message signatures
+      Right signature <- Either.throw . Signature.parse $ Ascii.pack "v1a,CV1O+PvrwXM42OMUX+tmm6bA3cS0tgLp0qo3YKuu0MGmBrsUhA0MHXF11HsEUJtPfTKs80WE7WUKVt9TueLDCQ=="
+      Test.assertEq test actual (Right signature)
+
+    Test.it test "fails with an invalid asymmetric signature" $ do
+      verifier <- Either.throw . Verifier.parse $ Ascii.pack "whpk_wuzPrKxPfWpJSsXgyg/MEoMGvjs5SjDO4ad6X4ZYqqg="
+      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
+      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1a,00000000000000000000000000000000000000000000000000000000000000000000000000000000000000=="
+      let actual = Verifier.verify verifier message signatures
+      Test.assertEq test actual (Left (VerificationException.MkVerificationException $ Message.id_ message))
+
+    Test.it test "succeeds with a valid symmetric signature" $ do
+      verifier <- Either.throw . Verifier.parse $ Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
+      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
+      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1,IywpE5NXy+JdAScgR7j5Pt59GjmazD7iJuVsQoRZFyw="
+      let actual = Verifier.verify verifier message signatures
+      Right signature <- Either.throw . Signature.parse $ Ascii.pack "v1,IywpE5NXy+JdAScgR7j5Pt59GjmazD7iJuVsQoRZFyw="
+      Test.assertEq test actual (Right signature)
+
+    Test.it test "fails with an invalid symmetric signature" $ do
+      verifier <- Either.throw . Verifier.parse $ Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
+      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
+      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1,0000000000000000000000000000000000000000000="
+      let actual = Verifier.verify verifier message signatures
+      Test.assertEq test actual (Left (VerificationException.MkVerificationException $ Message.id_ message))
+
+  Test.describe test "asymmetric" $ do
+    Test.it test "succeeds with a valid signature" $ do
+      publicKey <- Either.throw . PublicKey.parse $ Ascii.pack "whpk_wuzPrKxPfWpJSsXgyg/MEoMGvjs5SjDO4ad6X4ZYqqg="
+      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
+      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1a,CV1O+PvrwXM42OMUX+tmm6bA3cS0tgLp0qo3YKuu0MGmBrsUhA0MHXF11HsEUJtPfTKs80WE7WUKVt9TueLDCQ=="
+      let actual = Verifier.asymmetric publicKey message signatures
+      asymmetricSignature <- Either.throw . AsymmetricSignature.parse $ Ascii.pack "CV1O+PvrwXM42OMUX+tmm6bA3cS0tgLp0qo3YKuu0MGmBrsUhA0MHXF11HsEUJtPfTKs80WE7WUKVt9TueLDCQ=="
+      Test.assertEq test actual (Right asymmetricSignature)
+
+    Test.it test "fails with an invalid signature" $ do
+      publicKey <- Either.throw . PublicKey.parse $ Ascii.pack "whpk_wuzPrKxPfWpJSsXgyg/MEoMGvjs5SjDO4ad6X4ZYqqg="
+      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
+      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1a,00000000000000000000000000000000000000000000000000000000000000000000000000000000000000=="
+      let actual = Verifier.asymmetric publicKey message signatures
+      Test.assertEq test actual (Left (VerificationException.MkVerificationException $ Message.id_ message))
+
+  Test.describe test "symmetric" $ do
+    Test.it test "succeeds with a valid signature" $ do
+      secret <- Either.throw . Secret.parse $ Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
+      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
+      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1,IywpE5NXy+JdAScgR7j5Pt59GjmazD7iJuVsQoRZFyw="
+      let actual = Verifier.symmetric secret message signatures
+      symmetricSignature <- Either.throw . SymmetricSignature.parse $ Ascii.pack "IywpE5NXy+JdAScgR7j5Pt59GjmazD7iJuVsQoRZFyw="
+      Test.assertEq test actual (Right symmetricSignature)
+
+    Test.it test "fails with an invalid signature" $ do
+      secret <- Either.throw . Secret.parse $ Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
+      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
+      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1,0000000000000000000000000000000000000000000="
+      let actual = Verifier.symmetric secret message signatures
+      Test.assertEq test actual (Left (VerificationException.MkVerificationException $ Message.id_ message))
diff --git a/source/library/Signet/UnstableTest.hs b/source/library/Signet/UnstableTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Signet/UnstableTest.hs
@@ -0,0 +1,56 @@
+module Signet.UnstableTest where
+
+import qualified Data.ByteString.Char8 as Ascii
+import qualified Data.Time as Time
+import qualified Signet.Unstable as Signet
+import qualified Signet.Unstable.Exception.SignetException as SignetException
+import qualified Signet.Unstable.Exception.ToleranceException as ToleranceException
+import qualified Signet.Unstable.Exception.VerificationException as VerificationException
+import qualified Signet.Unstable.Extra.Either as Either
+import qualified Signet.Unstable.Type.Message as Message
+import qualified Signet.Unstable.Type.Signature as Signature
+import qualified Signet.Unstable.Type.Signatures as Signatures
+import qualified Signet.Unstable.Type.Test as Test
+import qualified Signet.Unstable.Type.Tolerance as Tolerance
+import qualified Signet.Unstable.Type.Verifier as Verifier
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = Test.describe test "Signet.Unstable" $ do
+  Test.describe test "verifyWebhookText" $ do
+    pure ()
+
+  Test.describe test "verifyWebhookByteString" $ do
+    pure ()
+
+  Test.describe test "verifyWebhook" $ do
+    pure ()
+
+  Test.describe test "verifyWebhookWith" $ do
+    Test.it test "succeeds with a valid symmetric signature" $ do
+      let tolerance = Tolerance.typical
+      let now = Time.UTCTime (Time.fromGregorian 1970 1 1) 0
+      verifier <- Either.throw . Verifier.parse $ Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
+      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
+      let byteString = Ascii.pack "v1,IywpE5NXy+JdAScgR7j5Pt59GjmazD7iJuVsQoRZFyw="
+      (_, signatures) <- Either.throw $ Signatures.parse byteString
+      let result = Signet.verifyWebhookWith tolerance now verifier message signatures
+      Right signature <- Either.throw $ Signature.parse byteString
+      Test.assertEq test result (Right signature)
+
+    Test.it test "fails with an invalid timestamp" $ do
+      let tolerance = Tolerance.typical
+      let now = Time.UTCTime (Time.fromGregorian 1970 1 1) 0
+      verifier <- Either.throw . Verifier.parse $ Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
+      message <- Either.throw . Message.parse $ Ascii.pack "i.301.Hello, world!"
+      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1,IywpE5NXy+JdAScgR7j5Pt59GjmazD7iJuVsQoRZFyw="
+      let result = Signet.verifyWebhookWith tolerance now verifier message signatures
+      Test.assertEq test result (Left (SignetException.ToleranceException . ToleranceException.MkToleranceException $ Message.timestamp message))
+
+    Test.it test "fails with an invalid symmetric signature" $ do
+      let tolerance = Tolerance.typical
+      let now = Time.UTCTime (Time.fromGregorian 1970 1 1) 0
+      verifier <- Either.throw . Verifier.parse $ Ascii.pack "whsec_bXlzZWNyZXRrZXkxMjM0NQ=="
+      message <- Either.throw . Message.parse $ Ascii.pack "i.0.Hello, world!"
+      (_, signatures) <- Either.throw . Signatures.parse $ Ascii.pack "v1,0000000000000000000000000000000000000000000="
+      let result = Signet.verifyWebhookWith tolerance now verifier message signatures
+      Test.assertEq test result (Left (SignetException.VerificationException . VerificationException.MkVerificationException $ Message.id_ message))
diff --git a/source/library/SignetTest.hs b/source/library/SignetTest.hs
new file mode 100644
--- /dev/null
+++ b/source/library/SignetTest.hs
@@ -0,0 +1,72 @@
+module SignetTest where
+
+import qualified Signet.Unstable.Exception.InvalidAsymmetricSignatureTest
+import qualified Signet.Unstable.Exception.InvalidIdTest
+import qualified Signet.Unstable.Exception.InvalidMessageTest
+import qualified Signet.Unstable.Exception.InvalidPublicKeyTest
+import qualified Signet.Unstable.Exception.InvalidSecretKeyTest
+import qualified Signet.Unstable.Exception.InvalidSecretTest
+import qualified Signet.Unstable.Exception.InvalidSignatureTest
+import qualified Signet.Unstable.Exception.InvalidSignerTest
+import qualified Signet.Unstable.Exception.InvalidSymmetricSignatureTest
+import qualified Signet.Unstable.Exception.InvalidTimestampTest
+import qualified Signet.Unstable.Exception.InvalidVerifierTest
+import qualified Signet.Unstable.Exception.SignetExceptionTest
+import qualified Signet.Unstable.Exception.ToleranceExceptionTest
+import qualified Signet.Unstable.Exception.VerificationExceptionTest
+import qualified Signet.Unstable.Extra.EitherTest
+import qualified Signet.Unstable.Extra.HttpTest
+import qualified Signet.Unstable.Extra.MaybeTest
+import qualified Signet.Unstable.Type.AsymmetricSignatureTest
+import qualified Signet.Unstable.Type.IdTest
+import qualified Signet.Unstable.Type.MessageTest
+import qualified Signet.Unstable.Type.PayloadTest
+import qualified Signet.Unstable.Type.PublicKeyTest
+import qualified Signet.Unstable.Type.SecretKeyTest
+import qualified Signet.Unstable.Type.SecretTest
+import qualified Signet.Unstable.Type.SignatureTest
+import qualified Signet.Unstable.Type.SignaturesTest
+import qualified Signet.Unstable.Type.SignerTest
+import qualified Signet.Unstable.Type.SymmetricSignatureTest
+import qualified Signet.Unstable.Type.Test as Test
+import qualified Signet.Unstable.Type.TimestampTest
+import qualified Signet.Unstable.Type.ToleranceTest
+import qualified Signet.Unstable.Type.UnknownSignatureTest
+import qualified Signet.Unstable.Type.VerifierTest
+import qualified Signet.UnstableTest
+
+spec :: (Monad tree) => Test.Test tree -> tree ()
+spec test = do
+  Signet.Unstable.Exception.InvalidAsymmetricSignatureTest.spec test
+  Signet.Unstable.Exception.InvalidIdTest.spec test
+  Signet.Unstable.Exception.InvalidMessageTest.spec test
+  Signet.Unstable.Exception.InvalidPublicKeyTest.spec test
+  Signet.Unstable.Exception.InvalidSecretKeyTest.spec test
+  Signet.Unstable.Exception.InvalidSecretTest.spec test
+  Signet.Unstable.Exception.InvalidSignatureTest.spec test
+  Signet.Unstable.Exception.InvalidSignerTest.spec test
+  Signet.Unstable.Exception.InvalidSymmetricSignatureTest.spec test
+  Signet.Unstable.Exception.InvalidTimestampTest.spec test
+  Signet.Unstable.Exception.InvalidVerifierTest.spec test
+  Signet.Unstable.Exception.SignetExceptionTest.spec test
+  Signet.Unstable.Exception.ToleranceExceptionTest.spec test
+  Signet.Unstable.Exception.VerificationExceptionTest.spec test
+  Signet.Unstable.Extra.EitherTest.spec test
+  Signet.Unstable.Extra.HttpTest.spec test
+  Signet.Unstable.Extra.MaybeTest.spec test
+  Signet.Unstable.Type.AsymmetricSignatureTest.spec test
+  Signet.Unstable.Type.IdTest.spec test
+  Signet.Unstable.Type.MessageTest.spec test
+  Signet.Unstable.Type.PayloadTest.spec test
+  Signet.Unstable.Type.PublicKeyTest.spec test
+  Signet.Unstable.Type.SecretKeyTest.spec test
+  Signet.Unstable.Type.SecretTest.spec test
+  Signet.Unstable.Type.SignaturesTest.spec test
+  Signet.Unstable.Type.SignatureTest.spec test
+  Signet.Unstable.Type.SignerTest.spec test
+  Signet.Unstable.Type.SymmetricSignatureTest.spec test
+  Signet.Unstable.Type.TimestampTest.spec test
+  Signet.Unstable.Type.ToleranceTest.spec test
+  Signet.Unstable.Type.UnknownSignatureTest.spec test
+  Signet.Unstable.Type.VerifierTest.spec test
+  Signet.UnstableTest.spec test
diff --git a/source/test-suite/Main.hs b/source/test-suite/Main.hs
--- a/source/test-suite/Main.hs
+++ b/source/test-suite/Main.hs
@@ -1,5 +1,19 @@
+import qualified Control.Monad.Trans.Writer as Writer
+import qualified Signet.Unstable.Type.Test as Test
 import qualified SignetTest
 import qualified Test.Tasty as Tasty
+import qualified Test.Tasty.HUnit as Unit
 
 main :: IO ()
-main = Tasty.defaultMain SignetTest.testTree
+main = Tasty.defaultMain testTree
+
+testTree :: Tasty.TestTree
+testTree = Tasty.testGroup "signet" . Writer.execWriter $ SignetTest.spec tasty
+
+tasty :: Test.Test (Writer.Writer [Tasty.TestTree])
+tasty =
+  Test.MkTest
+    { Test.assertFailure = Unit.assertFailure,
+      Test.describe = \x -> Writer.tell . pure . Tasty.testGroup x . Writer.execWriter,
+      Test.it = \x -> Writer.tell . pure . Unit.testCase x
+    }
