diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,4 @@
+# Change log
+
+Signet follows the [Package Versioning Policy](https://pvp.haskell.org). You can
+find release notes [on GitHub](https://github.com/MercuryTechnologies/signet/releases).
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,14 @@
+BSD Zero Clause License
+
+Copyright (c) 2025 Mercury Technologies, Inc.
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+# Signet
+
+Signet is a Haskell library for working with
+[Standard Webhooks](https://www.standardwebhooks.com).
diff --git a/signet.cabal b/signet.cabal
new file mode 100644
--- /dev/null
+++ b/signet.cabal
@@ -0,0 +1,165 @@
+cabal-version: 3.0
+name: signet
+version: 0.2025.5.5
+synopsis: Standard Webhooks
+description:
+  Signet is a Haskell library for working with Standard Webhooks.
+
+category: Web
+license: 0BSD
+license-file: LICENSE.txt
+maintainer: Mercury Technologies, Inc.
+extra-doc-files:
+  CHANGELOG.md
+  README.md
+
+source-repository head
+  location: https://github.com/MercuryTechnologies/signet
+  type: git
+
+flag pedantic
+  default: False
+  manual: True
+
+common library
+  build-depends: base ^>=4.19 || ^>=4.20 || ^>=4.21
+  default-language: Haskell2010
+  ghc-options:
+    -Weverything
+    -Wno-all-missed-specialisations
+    -Wno-implicit-prelude
+    -Wno-missed-specialisations
+    -Wno-missing-deriving-strategies
+    -Wno-missing-export-lists
+    -Wno-missing-kind-signatures
+    -Wno-missing-safe-haskell-mode
+    -Wno-prepositive-qualified-module
+    -Wno-safe
+    -Wno-unsafe
+
+  if flag(pedantic)
+    ghc-options: -Werror
+
+common executable
+  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,
+    text ^>=2.0 || ^>=2.1,
+
+  -- cabal-gild: discover source/libraries/unstable
+  exposed-modules:
+    Signet.Unstable
+    Signet.Unstable.Exception.InvalidAsymmetricSignature
+    Signet.Unstable.Exception.InvalidId
+    Signet.Unstable.Exception.InvalidMessage
+    Signet.Unstable.Exception.InvalidPublicKey
+    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
+
+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.InvalidSignatureTest
+    Signet.Unstable.Exception.InvalidSignerTest
+    Signet.Unstable.Exception.InvalidSymmetricSignatureTest
+    Signet.Unstable.Exception.InvalidTimestampTest
+    Signet.Unstable.Exception.InvalidVerifierTest
+    Signet.Unstable.Exception.SignetExceptionTest
+    Signet.Unstable.Exception.ToleranceExceptionTest
+    Signet.Unstable.Exception.VerificationExceptionTest
+    Signet.Unstable.Extra.EitherTest
+    Signet.Unstable.Extra.HttpTest
+    Signet.Unstable.Extra.MaybeTest
+    Signet.Unstable.Extra.Tasty
+    Signet.Unstable.Type.AsymmetricSignatureTest
+    Signet.Unstable.Type.IdTest
+    Signet.Unstable.Type.MessageTest
+    Signet.Unstable.Type.PayloadTest
+    Signet.Unstable.Type.PublicKeyTest
+    Signet.Unstable.Type.SecretKeyTest
+    Signet.Unstable.Type.SecretTest
+    Signet.Unstable.Type.SignatureTest
+    Signet.Unstable.Type.SignaturesTest
+    Signet.Unstable.Type.SignerTest
+    Signet.Unstable.Type.SymmetricSignatureTest
+    Signet.Unstable.Type.TimestampTest
+    Signet.Unstable.Type.ToleranceTest
+    Signet.Unstable.Type.UnknownSignatureTest
+    Signet.Unstable.Type.VerifierTest
+    Signet.UnstableTest
+    SignetTest
+
+  hs-source-dirs: source/libraries/test
+
+test-suite signet-test-suite
+  import: executable
+  build-depends:
+    signet:test,
+    tasty,
+
+  hs-source-dirs: source/test-suite
+  main-is: Main.hs
+  type: exitcode-stdio-1.0
diff --git a/source/libraries/signet/Signet.hs b/source/libraries/signet/Signet.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/signet/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/libraries/test/Signet/Unstable/Exception/InvalidAsymmetricSignatureTest.hs b/source/libraries/test/Signet/Unstable/Exception/InvalidAsymmetricSignatureTest.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Exception/InvalidAsymmetricSignatureTest.hs
@@ -0,0 +1,7 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Exception/InvalidIdTest.hs
@@ -0,0 +1,7 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Exception/InvalidMessageTest.hs
@@ -0,0 +1,7 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Exception/InvalidPublicKeyTest.hs
@@ -0,0 +1,7 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Exception/InvalidSecretKeyTest.hs
@@ -0,0 +1,7 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Exception/InvalidSecretTest.hs
@@ -0,0 +1,7 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Exception/InvalidSignatureTest.hs
@@ -0,0 +1,7 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Exception/InvalidSignerTest.hs
@@ -0,0 +1,7 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Exception/InvalidSymmetricSignatureTest.hs
@@ -0,0 +1,7 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Exception/InvalidTimestampTest.hs
@@ -0,0 +1,7 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Exception/InvalidVerifierTest.hs
@@ -0,0 +1,7 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Exception/SignetExceptionTest.hs
@@ -0,0 +1,7 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Exception/ToleranceExceptionTest.hs
@@ -0,0 +1,7 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Exception/VerificationExceptionTest.hs
@@ -0,0 +1,7 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Extra/EitherTest.hs
@@ -0,0 +1,31 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Extra/HttpTest.hs
@@ -0,0 +1,21 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Extra/MaybeTest.hs
@@ -0,0 +1,15 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Extra/Tasty.hs
@@ -0,0 +1,13 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Type/AsymmetricSignatureTest.hs
@@ -0,0 +1,27 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Type/IdTest.hs
@@ -0,0 +1,26 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Type/MessageTest.hs
@@ -0,0 +1,47 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Type/PayloadTest.hs
@@ -0,0 +1,7 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Type/PublicKeyTest.hs
@@ -0,0 +1,32 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Type/SecretKeyTest.hs
@@ -0,0 +1,32 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Type/SecretTest.hs
@@ -0,0 +1,31 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Type/SignatureTest.hs
@@ -0,0 +1,53 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Type/SignaturesTest.hs
@@ -0,0 +1,72 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Type/SignerTest.hs
@@ -0,0 +1,76 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Type/SymmetricSignatureTest.hs
@@ -0,0 +1,27 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Type/TimestampTest.hs
@@ -0,0 +1,33 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Type/ToleranceTest.hs
@@ -0,0 +1,34 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Type/UnknownSignatureTest.hs
@@ -0,0 +1,7 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/Unstable/Type/VerifierTest.hs
@@ -0,0 +1,110 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/Signet/UnstableTest.hs
@@ -0,0 +1,57 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/test/SignetTest.hs
@@ -0,0 +1,73 @@
+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
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Exception/InvalidAsymmetricSignature.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidAsymmetricSignature.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Exception/InvalidId.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidId.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Exception/InvalidMessage.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidMessage.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Exception/InvalidPublicKey.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidPublicKey.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Exception/InvalidSecret.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidSecret.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Exception/InvalidSecretKey.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidSecretKey.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Exception/InvalidSignature.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidSignature.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Exception/InvalidSigner.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidSigner.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Exception/InvalidSymmetricSignature.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidSymmetricSignature.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Exception/InvalidTimestamp.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidTimestamp.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Exception/InvalidVerifier.hs b/source/libraries/unstable/Signet/Unstable/Exception/InvalidVerifier.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Exception/SignetException.hs b/source/libraries/unstable/Signet/Unstable/Exception/SignetException.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Exception/ToleranceException.hs b/source/libraries/unstable/Signet/Unstable/Exception/ToleranceException.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Exception/VerificationException.hs b/source/libraries/unstable/Signet/Unstable/Exception/VerificationException.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Extra/Either.hs b/source/libraries/unstable/Signet/Unstable/Extra/Either.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Extra/Http.hs b/source/libraries/unstable/Signet/Unstable/Extra/Http.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Extra/Maybe.hs b/source/libraries/unstable/Signet/Unstable/Extra/Maybe.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Type/AsymmetricSignature.hs b/source/libraries/unstable/Signet/Unstable/Type/AsymmetricSignature.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Type/Id.hs b/source/libraries/unstable/Signet/Unstable/Type/Id.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Type/Message.hs b/source/libraries/unstable/Signet/Unstable/Type/Message.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Type/Payload.hs b/source/libraries/unstable/Signet/Unstable/Type/Payload.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Type/PublicKey.hs b/source/libraries/unstable/Signet/Unstable/Type/PublicKey.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Type/Secret.hs b/source/libraries/unstable/Signet/Unstable/Type/Secret.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Type/SecretKey.hs b/source/libraries/unstable/Signet/Unstable/Type/SecretKey.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Type/Signature.hs b/source/libraries/unstable/Signet/Unstable/Type/Signature.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Type/Signatures.hs b/source/libraries/unstable/Signet/Unstable/Type/Signatures.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Type/Signer.hs b/source/libraries/unstable/Signet/Unstable/Type/Signer.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Type/SymmetricSignature.hs b/source/libraries/unstable/Signet/Unstable/Type/SymmetricSignature.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Type/Timestamp.hs b/source/libraries/unstable/Signet/Unstable/Type/Timestamp.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Type/Tolerance.hs b/source/libraries/unstable/Signet/Unstable/Type/Tolerance.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Type/UnknownSignature.hs b/source/libraries/unstable/Signet/Unstable/Type/UnknownSignature.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/libraries/unstable/Signet/Unstable/Type/Verifier.hs b/source/libraries/unstable/Signet/Unstable/Type/Verifier.hs
new file mode 100644
--- /dev/null
+++ b/source/libraries/unstable/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/test-suite/Main.hs b/source/test-suite/Main.hs
new file mode 100644
--- /dev/null
+++ b/source/test-suite/Main.hs
@@ -0,0 +1,5 @@
+import qualified SignetTest
+import qualified Test.Tasty as Tasty
+
+main :: IO ()
+main = Tasty.defaultMain SignetTest.testTree
