diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,3 @@
+# Changelog for pagerduty
+
+## Unreleased changes
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Dustin Sallings (c) 2021
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Dustin Sallings nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+# pagerduty
+
+A Haskell interface for trigger, acknowledging, and resolving events
+at [PagerDuty](https://www.pagerduty.com).
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/pagerduty-hs.cabal b/pagerduty-hs.cabal
new file mode 100644
--- /dev/null
+++ b/pagerduty-hs.cabal
@@ -0,0 +1,71 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.34.4.
+--
+-- see: https://github.com/sol/hpack
+
+name:           pagerduty-hs
+version:        0.1.0.0
+description:    Please see the README on GitHub at <https://github.com/dustin/pagerduty#readme>
+homepage:       https://github.com/dustin/pagerduty-hs#readme
+bug-reports:    https://github.com/dustin/pagerduty-hs/issues
+author:         Dustin Sallings
+maintainer:     dustin@spy.net
+copyright:      Copyright © 2021 Dustin Sallings
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    ChangeLog.md
+
+source-repository head
+  type: git
+  location: https://github.com/dustin/pagerduty-hs
+
+library
+  exposed-modules:
+      Network.API.PagerDuty.EventV1
+  other-modules:
+      Paths_pagerduty_hs
+  hs-source-dirs:
+      src
+  default-extensions:
+      OverloadedStrings
+      RecordWildCards
+      NamedFieldPuns
+  ghc-options: -Wall -O2
+  build-depends:
+      aeson
+    , base >=4.7 && <5
+    , exceptions
+    , lens
+    , text
+    , wreq
+  default-language: Haskell2010
+
+test-suite pagerduty-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Paths_pagerduty_hs
+  hs-source-dirs:
+      test
+  default-extensions:
+      OverloadedStrings
+      RecordWildCards
+      NamedFieldPuns
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      HUnit
+    , aeson
+    , base >=4.7 && <5
+    , exceptions
+    , lens
+    , pagerduty-hsl
+    , tasty
+    , tasty-hunit
+    , tasty-quickcheck
+    , text
+    , wreq
+  default-language: Haskell2010
diff --git a/src/Network/API/PagerDuty/EventV1.hs b/src/Network/API/PagerDuty/EventV1.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/API/PagerDuty/EventV1.hs
@@ -0,0 +1,142 @@
+{-|
+Module      : Network.API.PagerDuty.EventV1
+Description : PagerDuty Event V1 interface.
+Copyright   : (c) Dustin Sallings, 2021
+License     : BSD3
+Maintainer  : dustin@spy.net
+Stability   : experimental
+
+PagerDuty Event V1 interface.
+-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Network.API.PagerDuty.EventV1 (
+  -- * Triggering an Event
+  TriggerEvent(..), TriggerEvent',
+  Context(..),
+  -- * Updating an Event
+  UpdateEvent(..), UpdateEvent', UpdateType(..),
+  -- * Delivering Events to PagerDuty
+  deliver, Response(..),
+  -- * Lenses and Prisms
+  _Link, _Image,
+  teServiceKey, teIncidentKey, teDescription, teDetails, teClient, teClientURL, teContexts,
+  _Acknowledge, _Resolve,
+  updateType, updateServiceKey, updateIncidentKey, updateDescription, updateDetails
+  ) where
+
+import           Control.Lens           (makeLenses, makePrisms, view)
+import           Control.Monad.Catch    (MonadCatch (..), SomeException (..), catch)
+import           Control.Monad.IO.Class (MonadIO (..))
+import           Data.Aeson             (FromJSON (..), ToJSON (..), Value (..), encode, object, (.:), (.=))
+import           Data.Aeson.Types       (Pair, typeMismatch)
+import           Data.Char              (toLower)
+import           Data.Maybe             (mapMaybe)
+import           Data.Text              (Text, pack)
+import           Network.Wreq           (asJSON, post, responseBody)
+import           Network.Wreq.Types     (Postable)
+
+class (ToJSON j) => EventRequest j
+
+-- | Context that may be added when creating an event.
+data Context = Link Text (Maybe Text) -- ^ Link to a URL with an optional link description.
+             | Image Text (Maybe Text) (Maybe Text) -- ^ Image URL, optional link ref, and optional alt text.
+  deriving (Show, Eq)
+
+makePrisms ''Context
+
+optj :: ToJSON v => [(Text, Maybe v)] -> [Pair]
+optj = mapMaybe (fmap (uncurry (.=)) . sequenceA)
+
+instance ToJSON Context where
+  toJSON (Link u t)      = object (["type" .= ("link" :: Text), "href" .= u] <> optj [("text", t)])
+  toJSON (Image s mu mt) = object (["type" .= ("image" :: Text), "src" .= s] <> optj [("href", mu), ("alt", mt)])
+
+-- | Request object to create an event.  Any value that may be
+-- serialized to JSON maybe attached as details.
+--
+-- This may be delivered with the 'deliver' function.
+data TriggerEvent a = TriggerEvent {
+  _teServiceKey    :: Text
+  , _teIncidentKey :: Maybe Text
+  , _teDescription :: Text
+  , _teDetails     :: Maybe a
+  , _teClient      :: Text
+  , _teClientURL   :: Text
+  , _teContexts    :: [Context]
+  }
+  deriving (Show, Eq)
+
+makeLenses ''TriggerEvent
+
+instance ToJSON a => EventRequest (TriggerEvent a)
+
+-- | A 'TriggerEvent' type that doesn't have details.
+type TriggerEvent' = TriggerEvent ()
+
+instance ToJSON a => ToJSON (TriggerEvent a) where
+  toJSON TriggerEvent{..} = object ([
+                                       "service_key" .= _teServiceKey
+                                    , "event_type" .= ("trigger"::Text)
+                                    , "description" .= _teDescription
+                                    , "client" .= _teClient
+                                    , "client_url" .= _teClientURL
+                                    ] <> optj [("details", _teDetails)]
+                                     <> optj [("incident_key", _teIncidentKey)]
+                                     <> opta "contexts" _teContexts
+                                   )
+    where opta _ [] = []
+          opta k vs = [k .= vs]
+
+-- | An event update will either acknowledge or resolve an incident.
+data UpdateType = Acknowledge | Resolve deriving (Show, Eq, Bounded, Enum)
+
+makePrisms ''UpdateType
+
+-- | UpdateEvent is the message for both acknowledging and resolving
+-- incidents.  This may be delivered using the 'deliver' function.
+data UpdateEvent a = UpdateEvent {
+  _updateType          :: UpdateType
+  , _updateServiceKey  :: Text
+  , _updateIncidentKey :: Text
+  , _updateDescription :: Text
+  , _updateDetails     :: Maybe a
+  }
+  deriving (Show, Eq)
+
+makeLenses ''UpdateEvent
+
+instance ToJSON a => EventRequest (UpdateEvent a)
+
+-- | A 'UpdateEvent' type that doesn't have details.
+type UpdateEvent' = UpdateEvent ()
+
+instance ToJSON a => ToJSON (UpdateEvent a) where
+  toJSON UpdateEvent{..} = object ([
+                                   "service_key" .= _updateServiceKey
+                                , "event_type" .= (map toLower . show) _updateType
+                                , "incident_key" .= _updateIncidentKey
+                                , "description" .= _updateDescription
+                                ] <> optj [("details", _updateDetails)])
+
+-- | Response to a delivered message.
+data Response = Failure Text Text -- ^ Failure status and message
+              | Success Text      -- ^ Success and incident key for further updates
+  deriving (Show, Eq)
+
+instance FromJSON Response where
+  parseJSON (Object v) = subparse =<< v .: "status"
+      where
+        subparse "success" = Success <$> v .: "incident_key"
+        subparse e         = Failure e <$> v .: "message"
+  parseJSON invalid = typeMismatch "Response" invalid
+
+jpost :: (MonadIO m, Postable a, FromJSON r) => String -> a -> m r
+jpost u v = view responseBody <$> liftIO (post u v >>= asJSON)
+
+-- | Deliver a 'TriggerEvent' or 'UpdateEvent'.
+deliver :: (EventRequest r, MonadCatch m, MonadIO m) => r -> m Response
+deliver r = send r `catch` failed
+  where
+    send = jpost "https://events.pagerduty.com/generic/2010-04-15/create_event.json" . encode
+    failed (SomeException e) = pure $ Failure "http exception" (pack (show e))
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,11 @@
+import           Test.Tasty
+import           Test.Tasty.HUnit
+import           Test.Tasty.QuickCheck as QC
+
+tests :: [TestTree]
+tests = [
+    testProperty "a test property" (\x -> (x::Int) == id x)
+    ]
+
+main :: IO ()
+main = defaultMain $ testGroup "All Tests" tests
