diff --git a/changelog.md b/changelog.md
new file mode 100644
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,12 @@
+# Changelog
+
+All notable changes to this package will be documented in this file.
+
+The format is based on
+[Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to the
+[Haskell Package Versioning Policy](https://pvp.haskell.org/).
+
+## 1.0.0.0 - 2018-12-20
+
+- Initial release
diff --git a/library/Stripe/Scotty.hs b/library/Stripe/Scotty.hs
new file mode 100644
--- /dev/null
+++ b/library/Stripe/Scotty.hs
@@ -0,0 +1,196 @@
+{-# OPTIONS_GHC -Wall #-}
+
+{- | For a typical webhook server implemented with Scotty, probably the only
+thing you need from this module is one of these two actions:
+
+  - 'requireSig' - If your live mode and test mode webhooks use separate
+    URLs.
+
+  - 'requireSig_eitherMode' - If your live mode and test mode webhooks use
+    the same URL. -}
+
+module Stripe.Scotty
+  ( requireSig, requireSig_eitherMode
+  , hasValidSig, getSig, getSigText
+  ) where
+
+-- aeson
+import qualified Data.Aeson
+
+-- base
+import Control.Monad (when, (>=>))
+
+-- bytestring
+import qualified Data.ByteString
+import qualified Data.ByteString.Lazy
+
+-- http-types
+import qualified Network.HTTP.Types.Status
+
+-- scotty
+import qualified Web.Scotty
+
+-- stripe-concepts
+import qualified Stripe.Concepts as Stripe
+
+-- stripe-signature
+import qualified Stripe.Signature as Stripe
+
+-- text
+import qualified Data.Text
+import qualified Data.Text.Lazy
+import qualified Data.Text.Lazy.Builder
+
+-- unordered-containers
+import qualified Data.HashMap.Strict
+
+{- | Terminates request processing if the request does not contain a valid
+Stripe signature header.
+
+This action returns the request body as a strict byte string; it has to fully
+evaluate the request body to do the signature check, so we might as well return
+this information to you for subsequent use. -}
+
+requireSig
+    :: Stripe.WebhookSecretKey
+    -> Web.Scotty.ActionM Data.ByteString.ByteString
+
+requireSig secret =
+  do
+    body <- getBody
+    okay <- hasValidSig secret body
+
+    when (not okay) invalidSigAction
+
+    return body
+
+{- | Terminates request processing if the request does not contain a valid
+Stripe signature header.
+
+This action returns the mode and JSON request body; it has to do this much
+parsing to determine the mode so that we can know which secret key to use in the
+verification, so we might as well return this information to you for subsequent
+use. -}
+
+requireSig_eitherMode
+    :: Stripe.BothModes (Maybe Stripe.WebhookSecretKey)
+    -> Web.Scotty.ActionM (Stripe.Mode, Data.Aeson.Value)
+
+requireSig_eitherMode secrets =
+  do
+    body   <- getBody
+    value  <- parseBody body
+    mode   <- getMode value
+    secret <- chooseSecret mode secrets
+    okay   <- hasValidSig secret body
+
+    when (not okay) invalidSigAction
+
+    return (mode, value)
+
+getBody :: Web.Scotty.ActionM Data.ByteString.ByteString
+getBody = Data.ByteString.Lazy.toStrict <$> Web.Scotty.body
+
+invalidSigAction :: Web.Scotty.ActionM a
+invalidSigAction =
+  do
+    Web.Scotty.status Network.HTTP.Types.Status.forbidden403
+    Web.Scotty.text (Data.Text.Lazy.pack "Invalid Stripe signature")
+    Web.Scotty.finish
+
+missingKeyAction :: Stripe.Mode -> Web.Scotty.ActionM a
+missingKeyAction mode =
+  do
+    Web.Scotty.status Network.HTTP.Types.Status.internalServerError500
+    Web.Scotty.text message
+    Web.Scotty.finish
+  where
+    message =
+        Data.Text.Lazy.Builder.toLazyText $
+            foldMap Data.Text.Lazy.Builder.fromString
+                [ "Configuration error: No webhook secret for "
+                , case mode of
+                    Stripe.LiveMode -> "live"
+                    Stripe.TestMode -> "test"
+                , " mode."
+                ]
+
+chooseSecret
+    :: Stripe.Mode
+    -> Stripe.BothModes (Maybe Stripe.WebhookSecretKey)
+    -> Web.Scotty.ActionM Stripe.WebhookSecretKey
+
+chooseSecret mode secrets =
+    case Stripe.applyMode mode secrets of
+        Just x -> return x
+        Nothing -> missingKeyAction mode
+
+parseBody
+    :: Data.ByteString.ByteString
+    -> Web.Scotty.ActionM Data.Aeson.Value
+
+parseBody bs =
+    case Data.Aeson.eitherDecode (Data.ByteString.Lazy.fromStrict bs) of
+        Left x ->
+          do
+            Web.Scotty.status Network.HTTP.Types.Status.badRequest400
+            Web.Scotty.text (Data.Text.Lazy.pack x)
+            Web.Scotty.finish
+        Right x ->
+            return x
+
+getMode :: Data.Aeson.Value -> Web.Scotty.ActionM Stripe.Mode
+getMode val =
+    case (aesonAttr "livemode" >=> aesonBool) val of
+
+      Nothing ->
+        do
+          Web.Scotty.status Network.HTTP.Types.Status.badRequest400
+          Web.Scotty.text (Data.Text.Lazy.pack
+              "Webhook attribute \"livemode\" is missing.")
+          Web.Scotty.finish
+
+      Just livemode ->
+          return (Stripe.isLiveMode' livemode)
+
+{- | Determines whether the request contains a valid Stripe signature header. -}
+
+hasValidSig
+    :: Stripe.WebhookSecretKey
+    -> Data.ByteString.ByteString
+    -> Web.Scotty.ActionM Bool
+
+hasValidSig secret body =
+  do
+    sigMaybe <- getSig
+    return $
+        case sigMaybe of
+            Nothing -> False
+            Just sig -> Stripe.isSigValid sig secret body
+
+getSigText :: Web.Scotty.ActionM (Maybe Data.Text.Text)
+getSigText =
+  do
+    x <- Web.Scotty.header (Data.Text.Lazy.pack "Stripe-Signature")
+    return (Data.Text.Lazy.toStrict <$> x)
+
+getSig :: Web.Scotty.ActionM (Maybe Stripe.Sig)
+getSig =
+  do
+    x <- getSigText
+    return (x >>= Stripe.parseSig)
+
+------------------------------------------------------------
+
+-- Internal Aeson decoding functions
+
+aesonAttr :: String -> Data.Aeson.Value -> Maybe Data.Aeson.Value
+aesonAttr x = aesonObject >=> Data.HashMap.Strict.lookup (Data.Text.pack x)
+
+aesonObject :: Data.Aeson.Value -> Maybe Data.Aeson.Object
+aesonObject (Data.Aeson.Object x) = Just x
+aesonObject _ = Nothing
+
+aesonBool :: Data.Aeson.Value -> Maybe Bool
+aesonBool (Data.Aeson.Bool x) = Just x
+aesonBool _ = Nothing
diff --git a/license.txt b/license.txt
new file mode 100644
--- /dev/null
+++ b/license.txt
@@ -0,0 +1,18 @@
+Copyright 2018 Typeclass Consulting, LLC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/stripe-scotty.cabal b/stripe-scotty.cabal
new file mode 100644
--- /dev/null
+++ b/stripe-scotty.cabal
@@ -0,0 +1,44 @@
+name: stripe-scotty
+version: 1.0.0.0
+
+synopsis: Listen for Stripe webhook events with Scotty
+category: Web
+
+description:
+    This package provides support for writing a
+    <https://stripe.com/ Stripe> webhook server using Scotty.
+
+homepage:    https://github.com/typeclasses/stripe
+bug-reports: https://github.com/typeclasses/stripe/issues
+
+author:     Chris Martin
+maintainer: Chris Martin, Julie Moronuki
+
+copyright: 2018 Typeclass Consulting, LLC
+license: MIT
+license-file: license.txt
+
+build-type: Simple
+cabal-version: >=1.10
+tested-with: GHC==8.2.2, GHC==8.4.3, GHC==8.6.1
+
+extra-source-files:
+    changelog.md
+
+library
+    hs-source-dirs: library
+    default-language: Haskell2010
+
+    exposed-modules:
+        Stripe.Scotty
+
+    build-depends:
+        aeson
+      , base >=4.10 && <5
+      , bytestring
+      , http-types
+      , scotty
+      , stripe-concepts
+      , stripe-signature
+      , text
+      , unordered-containers
