diff --git a/antiope-sns.cabal b/antiope-sns.cabal
--- a/antiope-sns.cabal
+++ b/antiope-sns.cabal
@@ -4,10 +4,10 @@
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: 82f8cb738c436e835125d6d461d17d61744a722f0d2625b158dfb193b08b1401
+-- hash: 50973aabaaad2ab917399cc0b8a236e46ccbb43b8534ed215c3cbcdae8fa42f6
 
 name:           antiope-sns
-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
@@ -29,6 +29,7 @@
 library
   exposed-modules:
       Antiope.SNS
+      Antiope.SNS.Messages
       Antiope.SNS.Types
   other-modules:
       Paths_antiope_sns
@@ -45,6 +46,7 @@
     , generic-lens
     , lens
     , text
+    , time
     , unliftio-core
   default-language: Haskell2010
 
@@ -67,5 +69,6 @@
     , generic-lens
     , lens
     , text
+    , time
     , unliftio-core
   default-language: Haskell2010
diff --git a/src/Antiope/SNS/Messages.hs b/src/Antiope/SNS/Messages.hs
new file mode 100644
--- /dev/null
+++ b/src/Antiope/SNS/Messages.hs
@@ -0,0 +1,35 @@
+{-# LANGUAGE DeriveGeneric #-}
+
+module Antiope.SNS.Messages
+  ( SnsMessage (..)
+  ) where
+
+import Data.Aeson      as Aeson
+import Data.Text       (Text)
+import Data.Time.Clock (UTCTime)
+import GHC.Generics    (Generic)
+
+import qualified Data.Aeson.Types   as Aeson
+import qualified Data.Text.Encoding as T
+
+data SnsMessage a = SnsMessage
+  { type'     :: !(Maybe Text)
+  , messageId :: !(Maybe Text)
+  , topicArn  :: !(Maybe Text)
+  , subject   :: !(Maybe Text)
+  , timestamp :: !(Maybe UTCTime)
+  , message   :: !a
+  } deriving (Show, Eq, Generic)
+
+instance FromJSON a => FromJSON (SnsMessage a) where
+  parseJSON = withObject "SnsMessage" $ \obj -> SnsMessage
+    <$> obj .:? "Type"
+    <*> obj .:? "MessageId"
+    <*> obj .:? "TopicArn"
+    <*> obj .:? "Subject"
+    <*> obj .:? "Timestamp"
+    <*> decodeEscaped obj "Message"
+
+decodeEscaped :: FromJSON b => Object -> Text -> Aeson.Parser b
+decodeEscaped o t =
+  (o .: t) >>= (either fail pure . eitherDecodeStrict . T.encodeUtf8)
