diff --git a/antiope-s3.cabal b/antiope-s3.cabal
--- a/antiope-s3.cabal
+++ b/antiope-s3.cabal
@@ -4,10 +4,10 @@
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: 114ac89b35e20a7f50e38154812d361792e7c7cade2c02278af6acbe429af480
+-- hash: 282b5f840ecbd3afbb1ec96fe2ccd47819d4fc98e6aa726a4bf9fc86863b8da1
 
 name:           antiope-s3
-version:        6.3.0
+version:        6.4.0
 description:    Please see the README on Github at <https://github.com/arbor/antiope#readme>
 category:       Services
 homepage:       https://github.com/arbor/antiope#readme
@@ -27,12 +27,25 @@
   location: https://github.com/arbor/antiope
 
 library
+  exposed-modules:
+      Antiope.S3
+      Antiope.S3.GetObject
+      Antiope.S3.Internal
+      Antiope.S3.Lazy
+      Antiope.S3.Messages
+      Antiope.S3.Range
+      Antiope.S3.Request
+      Antiope.S3.Strict
+      Antiope.S3.Types
+  other-modules:
+      Paths_antiope_s3
   hs-source-dirs:
       src
   default-extensions: BangPatterns GeneralizedNewtypeDeriving OverloadedStrings TupleSections
   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -msse4.2
   build-depends:
-      amazonka >=1.6
+      aeson
+    , amazonka >=1.6
     , amazonka-core >=1.6
     , amazonka-s3 >=1.6
     , antiope-core
@@ -50,29 +63,24 @@
     , network-uri
     , resourcet
     , text
+    , time
     , unliftio-core
-  exposed-modules:
-      Antiope.S3
-      Antiope.S3.GetObject
-      Antiope.S3.Internal
-      Antiope.S3.Lazy
-      Antiope.S3.Range
-      Antiope.S3.Request
-      Antiope.S3.Strict
-      Antiope.S3.Types
-  other-modules:
-      Paths_antiope_s3
   default-language: Haskell2010
 
 test-suite antiope-s3-test
   type: exitcode-stdio-1.0
   main-is: Spec.hs
+  other-modules:
+      Antiope.S3.TypesSpec
+      Antiope.S3Spec
+      Paths_antiope_s3
   hs-source-dirs:
       test
   default-extensions: BangPatterns GeneralizedNewtypeDeriving OverloadedStrings TupleSections
   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -msse4.2 -threaded -rtsopts -with-rtsopts=-N
   build-depends:
-      amazonka >=1.6
+      aeson
+    , amazonka >=1.6
     , amazonka-core >=1.6
     , amazonka-s3 >=1.6
     , antiope-core
@@ -94,9 +102,6 @@
     , network-uri
     , resourcet
     , text
+    , time
     , unliftio-core
-  other-modules:
-      Antiope.S3.TypesSpec
-      Antiope.S3Spec
-      Paths_antiope_s3
   default-language: Haskell2010
diff --git a/src/Antiope/S3/Messages.hs b/src/Antiope/S3/Messages.hs
new file mode 100644
--- /dev/null
+++ b/src/Antiope/S3/Messages.hs
@@ -0,0 +1,53 @@
+{-# LANGUAGE DeriveGeneric #-}
+
+module Antiope.S3.Messages
+  ( EventName(..)
+  , S3Message(..)
+  , readEventName
+  ) where
+
+import Antiope.S3      (BucketName (..), ETag (..), ObjectKey (..))
+import Data.Aeson      as Aeson
+import Data.Int        (Int64)
+import Data.Text       (Text)
+import Data.Time.Clock (UTCTime)
+import GHC.Generics    (Generic)
+
+import qualified Data.Text          as T
+import qualified Data.Text.Encoding as T
+
+data EventName = EventName
+  { eventType :: !Text
+  , action    :: !Text
+  } deriving (Show, Eq, Ord, Generic)
+
+data S3Message = S3Message
+  { eventTime :: !(Maybe UTCTime)
+  , eventName :: !EventName
+  , bucket    :: !BucketName
+  , key       :: !ObjectKey
+  , size      :: !Int64
+  , etag      :: !(Maybe ETag)
+  } deriving (Show, Eq, Generic)
+
+instance FromJSON S3Message where
+  parseJSON = withObject "S3Message" $ \obj -> do
+    eTime <- obj .:? "eventTime"
+    eName <- readEventName <$> obj .: "eventName"
+    s3    <- obj .: "s3"
+    bkt   <- s3 .: "bucket"
+    obj'  <- s3 .: "object"
+    bName <- BucketName <$> (bkt .: "name")
+    oKey  <- ObjectKey <$> (obj' .: "key")
+    oSize <- obj' .: "size"
+    oEtag  <- fmap (ETag . T.encodeUtf8) <$> (obj' .:? "eTag")
+    pure $ S3Message eTime eName bName oKey oSize oEtag
+
+readEventName :: T.Text -> EventName
+readEventName evt =
+  case T.splitOn ":" evt of
+    [t, e]       -> EventName t e
+    ["s3", t, e] -> EventName t e
+    (t:e:_)      -> EventName t e
+    _            -> EventName evt ""
+
