diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -36,6 +36,8 @@
 * Create `serverless.yml` with the following contents:
 
   ```yaml
+  service: myservice
+
   provider:
     name: aws
     runtime: nodejs6.10
diff --git a/serverless-haskell.cabal b/serverless-haskell.cabal
--- a/serverless-haskell.cabal
+++ b/serverless-haskell.cabal
@@ -2,10 +2,10 @@
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: 4c9e15e7f2028afe4a93b3dc54949d8501d4634b200ded5e15c1b7f2df5e358b
+-- hash: 39c6aa6699d4c9e7504dab750ed591876715bfaa355c00c418ad2b04a388de41
 
 name:           serverless-haskell
-version:        0.4.1
+version:        0.4.2
 synopsis:       Deploying Haskell code onto AWS Lambda using Serverless
 description:    Utilities to help process the events from AWS Lambda when deployed with the serverless-haskell plugin.
 category:       AWS, Cloud, Network
@@ -31,11 +31,13 @@
   build-depends:
       aeson
     , aeson-casing
+    , aeson-extra
     , amazonka-core
     , amazonka-kinesis
     , amazonka-s3
     , base >=4.7 && <5
     , bytestring
+    , case-insensitive
     , lens
     , text
     , time
@@ -44,6 +46,7 @@
   exposed-modules:
       AWSLambda
       AWSLambda.Events
+      AWSLambda.Events.APIGateway
       AWSLambda.Events.KinesisEvent
       AWSLambda.Events.Records
       AWSLambda.Events.S3Event
@@ -52,6 +55,7 @@
       AWSLambda.Orphans
       Data.Aeson.Alternative
       Data.Aeson.Embedded
+      Data.Aeson.TextValue
   other-modules:
       Paths_serverless_haskell
   default-language: Haskell2010
@@ -61,14 +65,17 @@
   main-is: Spec.hs
   hs-source-dirs:
       test
+  ghc-options: -Wall
   build-depends:
       aeson
     , aeson-casing
+    , aeson-extra
     , amazonka-core
     , amazonka-kinesis
     , amazonka-s3
     , base >=4.7 && <5
     , bytestring
+    , case-insensitive
     , hspec
     , hspec-discover
     , lens
@@ -79,6 +86,7 @@
     , unix
     , unordered-containers
   other-modules:
+      AWSLambda.Events.APIGatewaySpec
       AWSLambda.Events.KinesisEventSpec
       AWSLambda.Events.S3EventSpec
       AWSLambda.Events.SNSEventSpec
diff --git a/src/AWSLambda/Events.hs b/src/AWSLambda/Events.hs
--- a/src/AWSLambda/Events.hs
+++ b/src/AWSLambda/Events.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE DeriveGeneric   #-}
 {-# LANGUAGE TemplateHaskell #-}
 
 module AWSLambda.Events where
@@ -5,6 +6,7 @@
 import           Control.Applicative           ((<|>))
 import           Control.Lens.TH
 import           Data.Aeson                    (FromJSON (..), Value)
+import           Network.AWS.Data.Text         (FromText)
 
 import           AWSLambda.Events.KinesisEvent
 import           AWSLambda.Events.S3Event
@@ -82,7 +84,7 @@
 -- | Attempt to parse the various event types.
 -- Any valid JSON that can't be parsed as a specific
 -- event type will result in a 'Custom' value.
-instance FromJSON snsMessage =>
+instance FromText snsMessage =>
          FromJSON (LambdaEvent snsMessage) where
   parseJSON v =
     try S3 v <|> try KinesisStream v <|> try SNS v <|> pure (Custom v)
diff --git a/src/AWSLambda/Events/APIGateway.hs b/src/AWSLambda/Events/APIGateway.hs
new file mode 100644
--- /dev/null
+++ b/src/AWSLambda/Events/APIGateway.hs
@@ -0,0 +1,182 @@
+{-# LANGUAGE DeriveGeneric     #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+
+{-|
+Module: AWSLambda.Events.APIGateway
+Description: Types for APIGateway Lambda requests and responses
+
+Based on https://github.com/aws/aws-lambda-dotnet/tree/master/Libraries/src/Amazon.Lambda.APIGatewayEvents
+-}
+module AWSLambda.Events.APIGateway where
+
+import           Control.Lens
+import           Data.Aeson
+import           Data.Aeson.Casing       (aesonDrop, camelCase)
+import           Data.Aeson.TH           (deriveFromJSON)
+-- import           Data.CaseInsensitive (CI (..))
+import           Data.Aeson.Embedded
+import           Data.Aeson.TextValue
+import           Data.ByteString         (ByteString)
+import           Data.HashMap.Strict     (HashMap)
+import qualified Data.HashMap.Strict     as HashMap
+import           Data.Text               (Text)
+import           GHC.Generics            (Generic)
+import           Network.AWS.Data.Base64
+import           Network.AWS.Data.Text
+
+import           AWSLambda.Handler       (lambdaMain)
+
+type Method = Text
+-- type HeaderName = CI Text
+type HeaderName = Text --- XXX should be CI Text
+type HeaderValue = Text
+type QueryParamName = Text
+type QueryParamValue = Text
+type PathParamName = Text
+type PathParamValue = Text
+type StageVarName = Text
+type StageVarValue = Text
+
+data RequestIdentity = RequestIdentity
+  { _riCognitoIdentityPoolId         :: !(Maybe Text)
+  , _riAccountId                     :: !(Maybe Text)
+  , _riCognitoIdentityId             :: !(Maybe Text)
+  , _riCaller                        :: !(Maybe Text)
+  , _riApiKey                        :: !(Maybe Text)
+  , _riSourceIp                      :: !(Maybe Text)
+  , _riCognitoAuthenticationType     :: !(Maybe Text)
+  , _riCognitoAuthenticationProvider :: !(Maybe Text)
+  , _riUserArn                       :: !(Maybe Text)
+  , _riUserAgent                     :: !(Maybe Text)
+  , _riUser                          :: !(Maybe Text)
+  } deriving (Eq, Show)
+
+$(deriveFromJSON (aesonDrop 3 camelCase) ''RequestIdentity)
+$(makeLenses ''RequestIdentity)
+
+data ProxyRequestContext = ProxyRequestContext
+  { _prcPath         :: !(Maybe Text)
+  , _prcAccountId    :: !Text
+  , _prcResourceId   :: !Text
+  , _prcStage        :: !Text
+  , _prcRequestId    :: !Text
+  , _prcIdentity     :: !RequestIdentity
+  , _prcResourcePath :: !Text
+  , _prcHttpMethod   :: !Text
+  , _prcApiId        :: !Text
+  } deriving (Eq, Show)
+$(deriveFromJSON (aesonDrop 4 camelCase) ''ProxyRequestContext)
+$(makeLenses ''ProxyRequestContext)
+
+data APIGatewayProxyRequest body = APIGatewayProxyRequest
+  { _agprqResource              :: !Text
+  , _agprqPath                  :: !Text
+  , _agprqHttpMethod            :: !Method
+  , _agprqHeaders               :: !(HashMap HeaderName HeaderValue)
+  , _agprqQueryStringParameters :: !(HashMap QueryParamName QueryParamValue)
+  , _agprqPathParameters        :: !(HashMap PathParamName PathParamValue)
+  , _agprqStageVariables        :: !(HashMap StageVarName StageVarValue)
+  , _agprqRequestContext        :: !ProxyRequestContext
+  , _agprqBody                  :: !(Maybe (TextValue body))
+  } deriving (Eq, Show, Generic)
+
+instance FromText body => FromJSON (APIGatewayProxyRequest body) where
+  parseJSON = withObject "APIGatewayProxyRequest" $ \o ->
+    APIGatewayProxyRequest
+    <$> o .: "resource"
+    <*> o .: "path"
+    <*> o .: "httpMethod"
+    <*> o .:? "headers" .!= HashMap.empty
+    <*> o .:? "queryStringParameters" .!= HashMap.empty
+    <*> o .:? "pathParameters" .!= HashMap.empty
+    <*> o .:? "stageVariables" .!= HashMap.empty
+    <*> o .: "requestContext"
+    <*> o .:? "body"
+
+
+$(makeLenses ''APIGatewayProxyRequest)
+
+-- | Get the request body, if there is one
+requestBody :: Getter (APIGatewayProxyRequest body) (Maybe body)
+requestBody = agprqBody . mapping unTextValue
+
+-- | Get the embedded request body, if there is one
+requestBodyEmbedded :: Getter (APIGatewayProxyRequest (Embedded v)) (Maybe v)
+requestBodyEmbedded = requestBody . mapping unEmbed
+
+-- | Get the binary (decoded Base64) request body, if there is one
+requestBodyBinary :: Getter (APIGatewayProxyRequest Base64) (Maybe ByteString)
+requestBodyBinary = requestBody . mapping _Base64
+
+data APIGatewayProxyResponse body = APIGatewayProxyResponse
+  { _agprsStatusCode :: !Int
+  , _agprsHeaders    :: !(HashMap HeaderName HeaderValue)
+  , _agprsBody       :: !(Maybe (TextValue body))
+  } deriving (Eq, Show, Generic)
+
+instance ToText body => ToJSON (APIGatewayProxyResponse body) where
+  toJSON = genericToJSON $ aesonDrop 6 camelCase
+
+instance FromText body => FromJSON (APIGatewayProxyResponse body) where
+  parseJSON = genericParseJSON $ aesonDrop 6 camelCase
+
+$(makeLenses ''APIGatewayProxyResponse)
+
+response :: Int -> APIGatewayProxyResponse body
+response statusCode = APIGatewayProxyResponse statusCode HashMap.empty Nothing
+
+responseOK :: APIGatewayProxyResponse body
+responseOK = response 200
+
+responseNotFound :: APIGatewayProxyResponse body
+responseNotFound = response 404
+
+responseBadRequest :: APIGatewayProxyResponse body
+responseBadRequest = response 400
+
+responseBody :: Setter' (APIGatewayProxyResponse body) (Maybe body)
+responseBody = agprsBody . at () . mapping unTextValue
+
+responseBodyEmbedded :: Setter' (APIGatewayProxyResponse (Embedded body)) (Maybe body)
+responseBodyEmbedded = responseBody . mapping unEmbed
+
+responseBodyBinary :: Setter' (APIGatewayProxyResponse Base64) (Maybe ByteString)
+responseBodyBinary = responseBody . mapping _Base64
+
+-- | Process incoming events from @serverless-haskell@ using a provided
+-- function.
+--
+-- This is a specialisation of lambdaMain for API Gateway.
+--
+-- The handler receives the input event given to the AWS Lambda function, and
+-- its return value is returned from the function.
+--
+-- This is intended to be used as @main@, for example:
+--
+-- > import AWSLambda.Events.APIGateway
+-- > import Control.Lens
+-- > import Data.Aeson
+-- > import Data.Aeson.Embedded
+-- >
+-- > main = apiGatewayMain handler
+-- >
+-- > handler :: APIGatewayProxyRequest (Embedded Value) -> IO (APIGatewayProxyResponse (Embedded [Int]))
+-- > handler request = do
+-- >   putStrLn "This should go to logs"
+-- >   print $ request ^. requestBody
+-- >   pure $ responseOK & responseBodyEmbedded ?~ [1, 2, 3]
+--
+-- The type parameters @reqBody@ and @resBody@ represent the types of request and response body, respectively.
+-- The @FromText@ and @ToText@ contraints are required because these values come from string fields
+-- in the request and response JSON objects.
+-- To get direct access to the body string, use @Text@ as the parameter type.
+-- To treat the body as a stringified embedded JSON value, use @Embedded a@, where @a@ has the
+-- appropriate @FromJSON@ or @ToJSON@ instances.
+-- To treat the body as base 64 encoded binary use @Base64@.
+apiGatewayMain
+  :: (FromText reqBody, ToText resBody)
+  => (APIGatewayProxyRequest reqBody -> IO (APIGatewayProxyResponse resBody)) -- ^ Function to process the event
+  -> IO ()
+apiGatewayMain = lambdaMain
diff --git a/src/AWSLambda/Events/SNSEvent.hs b/src/AWSLambda/Events/SNSEvent.hs
--- a/src/AWSLambda/Events/SNSEvent.hs
+++ b/src/AWSLambda/Events/SNSEvent.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE DeriveGeneric     #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE RecordWildCards   #-}
 {-# LANGUAGE TemplateHaskell   #-}
@@ -10,17 +11,24 @@
 -}
 module AWSLambda.Events.SNSEvent where
 
-import           Control.Lens.TH
+import           Control.Lens
+import           Data.Aeson               (FromJSON (..), genericParseJSON)
 import           Data.Aeson.Casing        (aesonDrop, pascalCase)
+import           Data.Aeson.Embedded
+import           Data.Aeson.TextValue
 import           Data.Aeson.TH            (deriveFromJSON)
+import           Data.ByteString          (ByteString)
 import           Data.HashMap.Strict      (HashMap)
 import           Data.Text                (Text)
 import           Data.Time.Clock          (UTCTime)
+import           GHC.Generics             (Generic)
+import           Network.AWS.Data.Base64
+import           Network.AWS.Data.Text    (FromText)
 
 import           AWSLambda.Events.Records
 
 data MessageAttribute = MessageAttribute
-  { _maType :: !Text
+  { _maType  :: !Text
   , _maValue :: !Text
   } deriving (Eq, Show)
 
@@ -28,7 +36,7 @@
 $(makeLenses ''MessageAttribute)
 
 data SNSMessage message = SNSMessage
-  { _smMessage           :: !message
+  { _smMessage           :: !(TextValue message )
   , _smMessageAttributes :: !(HashMap Text MessageAttribute)
   , _smMessageId         :: !Text
   , _smSignature         :: !Text
@@ -39,9 +47,11 @@
   , _smTopicArn          :: !Text
   , _smType              :: !Text
   , _smUnsubscribeUrl    :: !Text
-  } deriving (Eq, Show)
+  } deriving (Eq, Show, Generic)
 
-$(deriveFromJSON (aesonDrop 3 pascalCase) ''SNSMessage)
+instance FromText message => FromJSON (SNSMessage message) where
+  parseJSON = genericParseJSON $ aesonDrop 3 pascalCase
+
 $(makeLenses ''SNSMessage)
 
 data SNSRecord message = SNSRecord
@@ -49,9 +59,11 @@
   , _srEventSubscriptionArn :: !Text
   , _srEventSource          :: !Text
   , _srSns                  :: !(SNSMessage message)
-  } deriving (Eq, Show)
+  } deriving (Eq, Show, Generic)
 
-$(deriveFromJSON (aesonDrop 3 pascalCase) ''SNSRecord)
+instance FromText message => FromJSON (SNSRecord message) where
+  parseJSON = genericParseJSON $ aesonDrop 3 pascalCase
+
 $(makeLenses ''SNSRecord)
 
 -- | SNSEvent.
@@ -60,4 +72,17 @@
 -- To extract an embedded event object use the 'Embedded' type.
 -- E.g. @SNSEvent (Embedded S3Event)@ will treat the message
 -- as an embedded S3Event.
+-- To extract embedded Base64 encoded binary use
+-- @SNSEvent Base64@
 type SNSEvent message = RecordsEvent (SNSRecord message)
+
+-- | A Traversal to get messages from an SNSEvent
+messages :: Traversal (SNSEvent message) (SNSEvent message') message message'
+messages = reRecords . traverse . srSns . smMessage . unTextValue
+
+-- | A Traversal to get embedded JSON values from an SNSEvent
+embedded :: Traversal (SNSEvent (Embedded v)) (SNSEvent (Embedded v')) v v'
+embedded = messages . unEmbed
+
+binary :: Traversal' (SNSEvent Base64) ByteString
+binary = messages . _Base64
diff --git a/src/Data/Aeson/Embedded.hs b/src/Data/Aeson/Embedded.hs
--- a/src/Data/Aeson/Embedded.hs
+++ b/src/Data/Aeson/Embedded.hs
@@ -1,6 +1,7 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards   #-}
-{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE RecordWildCards            #-}
+{-# LANGUAGE TemplateHaskell            #-}
 
 {-|
 Module: Data.Aeson.Embedded
@@ -10,22 +11,28 @@
 
 import           Control.Lens.TH
 import           Data.Aeson
-import           Data.Text.Lazy            (Text)
-import           Data.Text.Lazy.Encoding   (decodeUtf8, encodeUtf8)
+import           Data.Aeson.Extra      (encodeStrict)
+import           Data.Text.Encoding    (decodeUtf8, encodeUtf8)
+import           Network.AWS.Data.Text (FromText (..), ToText (..), fromText,
+                                        takeText)
 
 -- | Type for a JSON value embedded within a JSON string value
 newtype Embedded a = Embedded { _unEmbed :: a } deriving (Eq, Show)
 
 instance FromJSON a =>
+         FromText (Embedded a) where
+  parser =
+    fmap Embedded . either fail pure . eitherDecodeStrict . encodeUtf8 =<< takeText
+
+instance FromJSON a =>
          FromJSON (Embedded a) where
-  parseJSON v =
-    fmap Embedded . either fail pure . eitherDecode . encodeUtf8 =<< parseJSON v
+  parseJSON v = either fail pure . fromText =<< parseJSON v
 
-instance ToJSON a => ToJSON (Embedded a) where
-  toJSON = toJSON . embed
-  toEncoding = toEncoding . embed
+instance ToJSON a => ToText (Embedded a) where
+  toText = decodeUtf8 . encodeStrict . _unEmbed
 
-embed :: ToJSON a => Embedded a -> Text
-embed = decodeUtf8 . encode . _unEmbed
+instance ToJSON a => ToJSON (Embedded a) where
+  toJSON = toJSON . toText
+  toEncoding = toEncoding . toText
 
 $(makeLenses ''Embedded)
diff --git a/src/Data/Aeson/TextValue.hs b/src/Data/Aeson/TextValue.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Aeson/TextValue.hs
@@ -0,0 +1,32 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE RecordWildCards            #-}
+{-# LANGUAGE TemplateHaskell            #-}
+
+{-|
+Module: Data.Aeson.TextValue
+Description: Type for things that can be embedded in a JSON string
+
+Provides @FromJSON@ and @ToJSON@ instances for anything that
+has @FromText@ and @ToText@ instances, e.g. @TextValue Text@,
+@(FromJSON a, ToJSON a) => TextValue (Embedded a)@,
+@TextValue Base64@
+-}
+module Data.Aeson.TextValue where
+
+import           Control.Lens.TH
+import           Data.Aeson
+import           Data.String
+import           Network.AWS.Data.Text (FromText (..), ToText (..), fromText)
+
+
+newtype TextValue a = TextValue { _unTextValue :: a } deriving (Eq, Show, IsString)
+
+instance FromText a => FromJSON (TextValue a) where
+  parseJSON = withText "TextValue" $ fmap TextValue . either fail pure . fromText
+
+instance ToText a => ToJSON (TextValue a) where
+  toJSON = toJSON . toText . _unTextValue
+  toEncoding = toEncoding . toText . _unTextValue
+
+$(makeLenses ''TextValue)
diff --git a/test/AWSLambda/Events/APIGatewaySpec.hs b/test/AWSLambda/Events/APIGatewaySpec.hs
new file mode 100644
--- /dev/null
+++ b/test/AWSLambda/Events/APIGatewaySpec.hs
@@ -0,0 +1,199 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+
+module AWSLambda.Events.APIGatewaySpec where
+
+import           AWSLambda.Events.APIGateway
+
+import           Control.Lens
+import           Data.Aeson
+import           Data.ByteString.Lazy        (ByteString)
+import qualified Data.HashMap.Strict         as HashMap
+import           Data.Text                   (Text)
+
+import           Text.RawString.QQ
+
+import           Test.Hspec
+
+spec :: Spec
+spec =  do
+  describe "APIGatewayProxyRequest" $
+    it "parses sample GET request" $
+      decode sampleGetRequestJSON `shouldBe` Just sampleGetRequest
+  describe "APIGatewayProxyResponse" $
+    it "parses sample text event" $
+      decode sampleOKResponseJSON `shouldBe` Just sampleOKResponse
+
+sampleGetRequestJSON :: ByteString
+sampleGetRequestJSON = [r|
+{
+  "path": "/test/hello",
+  "headers": {
+    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
+    "Accept-Encoding": "gzip, deflate, lzma, sdch, br",
+    "Accept-Language": "en-US,en;q=0.8",
+    "CloudFront-Forwarded-Proto": "https",
+    "CloudFront-Is-Desktop-Viewer": "true",
+    "CloudFront-Is-Mobile-Viewer": "false",
+    "CloudFront-Is-SmartTV-Viewer": "false",
+    "CloudFront-Is-Tablet-Viewer": "false",
+    "CloudFront-Viewer-Country": "US",
+    "Host": "wt6mne2s9k.execute-api.us-west-2.amazonaws.com",
+    "Upgrade-Insecure-Requests": "1",
+    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48",
+    "Via": "1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)",
+    "X-Amz-Cf-Id": "nBsWBOrSHMgnaROZJK1wGCZ9PcRcSpq_oSXZNQwQ10OTZL4cimZo3g==",
+    "X-Forwarded-For": "192.168.100.1, 192.168.1.1",
+    "X-Forwarded-Port": "443",
+    "X-Forwarded-Proto": "https"
+  },
+  "pathParameters": {
+    "proxy": "hello"
+  },
+  "requestContext": {
+    "accountId": "123456789012",
+    "resourceId": "us4z18",
+    "stage": "test",
+    "requestId": "41b45ea3-70b5-11e6-b7bd-69b5aaebc7d9",
+    "identity": {
+      "cognitoIdentityPoolId": "",
+      "accountId": "",
+      "cognitoIdentityId": "",
+      "caller": "",
+      "apiKey": "",
+      "sourceIp": "192.168.100.1",
+      "cognitoAuthenticationType": "",
+      "cognitoAuthenticationProvider": "",
+      "userArn": "",
+      "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48",
+      "user": ""
+    },
+    "resourcePath": "/{proxy+}",
+    "httpMethod": "GET",
+    "apiId": "wt6mne2s9k"
+  },
+  "resource": "/{proxy+}",
+  "httpMethod": "GET",
+  "queryStringParameters": {
+    "name": "me"
+  },
+  "stageVariables": {
+    "stageVarName": "stageVarValue"
+  }
+}
+|]
+
+sampleGetRequest :: APIGatewayProxyRequest Text
+sampleGetRequest =
+  APIGatewayProxyRequest
+  { _agprqResource = "/{proxy+}"
+  , _agprqPath = "/test/hello"
+  , _agprqHttpMethod = "GET"
+  , _agprqHeaders =
+    HashMap.fromList
+      [ ("X-Forwarded-Proto", "https")
+      , ("CloudFront-Is-Desktop-Viewer", "true")
+      , ( "Accept"
+        , "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
+      , ( "X-Amz-Cf-Id"
+        , "nBsWBOrSHMgnaROZJK1wGCZ9PcRcSpq_oSXZNQwQ10OTZL4cimZo3g==")
+      , ("Accept-Encoding", "gzip, deflate, lzma, sdch, br")
+      , ("CloudFront-Forwarded-Proto", "https")
+      , ("Accept-Language", "en-US,en;q=0.8")
+      , ("CloudFront-Is-Tablet-Viewer", "false")
+      , ("Upgrade-Insecure-Requests", "1")
+      , ("CloudFront-Viewer-Country", "US")
+      , ( "User-Agent"
+        , "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48")
+      , ("CloudFront-Is-Mobile-Viewer", "false")
+      , ("Host", "wt6mne2s9k.execute-api.us-west-2.amazonaws.com")
+      , ("X-Forwarded-Port", "443")
+      , ("CloudFront-Is-SmartTV-Viewer", "false")
+      , ( "Via"
+        , "1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)")
+      , ("X-Forwarded-For", "192.168.100.1, 192.168.1.1")
+      ]
+  , _agprqQueryStringParameters = HashMap.fromList [("name", "me")]
+  , _agprqPathParameters = HashMap.fromList [("proxy", "hello")]
+  , _agprqStageVariables = HashMap.fromList [("stageVarName", "stageVarValue")]
+  , _agprqRequestContext =
+    ProxyRequestContext
+    { _prcPath = Nothing
+    , _prcAccountId = "123456789012"
+    , _prcResourceId = "us4z18"
+    , _prcStage = "test"
+    , _prcRequestId = "41b45ea3-70b5-11e6-b7bd-69b5aaebc7d9"
+    , _prcIdentity =
+      RequestIdentity
+      { _riCognitoIdentityPoolId = Just ""
+      , _riAccountId = Just ""
+      , _riCognitoIdentityId = Just ""
+      , _riCaller = Just ""
+      , _riApiKey = Just ""
+      , _riSourceIp = Just "192.168.100.1"
+      , _riCognitoAuthenticationType = Just ""
+      , _riCognitoAuthenticationProvider = Just ""
+      , _riUserArn = Just ""
+      , _riUserAgent = Just
+        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48"
+      , _riUser = Just ""
+      }
+    , _prcResourcePath = "/{proxy+}"
+    , _prcHttpMethod = "GET"
+    , _prcApiId = "wt6mne2s9k"
+    }
+  , _agprqBody = Nothing
+  }
+
+sampleOKResponseJSON :: ByteString
+sampleOKResponseJSON = [r|
+{
+  "statusCode": 200,
+  "headers": {
+    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
+    "Accept-Encoding": "gzip, deflate, lzma, sdch, br",
+    "Accept-Language": "en-US,en;q=0.8",
+    "CloudFront-Forwarded-Proto": "https",
+    "CloudFront-Is-Desktop-Viewer": "true",
+    "CloudFront-Is-Mobile-Viewer": "false",
+    "CloudFront-Is-SmartTV-Viewer": "false",
+    "CloudFront-Is-Tablet-Viewer": "false",
+    "CloudFront-Viewer-Country": "US",
+    "Host": "wt6mne2s9k.execute-api.us-west-2.amazonaws.com",
+    "Upgrade-Insecure-Requests": "1",
+    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48",
+    "Via": "1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)",
+    "X-Amz-Cf-Id": "nBsWBOrSHMgnaROZJK1wGCZ9PcRcSpq_oSXZNQwQ10OTZL4cimZo3g==",
+    "X-Forwarded-For": "192.168.100.1, 192.168.1.1",
+    "X-Forwarded-Port": "443",
+    "X-Forwarded-Proto": "https"
+  },
+  "body": "Hello World"
+}
+|]
+
+sampleOKResponse :: APIGatewayProxyResponse Text
+sampleOKResponse =
+  responseOK
+  & responseBody ?~ "Hello World"
+  & agprsHeaders .~ HashMap.fromList
+    [ ("X-Forwarded-Proto", "https")
+    , ("CloudFront-Is-Desktop-Viewer", "true")
+    , ( "Accept"
+      , "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
+    , ("X-Amz-Cf-Id", "nBsWBOrSHMgnaROZJK1wGCZ9PcRcSpq_oSXZNQwQ10OTZL4cimZo3g==")
+    , ("Accept-Encoding", "gzip, deflate, lzma, sdch, br")
+    , ("CloudFront-Forwarded-Proto", "https")
+    , ("Accept-Language", "en-US,en;q=0.8")
+    , ("CloudFront-Is-Tablet-Viewer", "false")
+    , ("Upgrade-Insecure-Requests", "1")
+    , ("CloudFront-Viewer-Country", "US")
+    , ( "User-Agent"
+      , "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48")
+    , ("CloudFront-Is-Mobile-Viewer", "false")
+    , ("Host", "wt6mne2s9k.execute-api.us-west-2.amazonaws.com")
+    , ("X-Forwarded-Port", "443")
+    , ("CloudFront-Is-SmartTV-Viewer", "false")
+    , ("Via", "1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)")
+    , ("X-Forwarded-For", "192.168.100.1, 192.168.1.1")
+    ]
diff --git a/test/AWSLambda/Events/SNSEventSpec.hs b/test/AWSLambda/Events/SNSEventSpec.hs
--- a/test/AWSLambda/Events/SNSEventSpec.hs
+++ b/test/AWSLambda/Events/SNSEventSpec.hs
@@ -9,13 +9,13 @@
 
 import           Data.Aeson
 import           Data.Aeson.Embedded
+import           Data.Aeson.TextValue
 import           Data.ByteString.Lazy      (ByteString)
 import qualified Data.HashMap.Strict       as HashMap
 import           Data.Text                 (Text)
 import           Data.Time.Calendar
 import           Data.Time.Clock
 import           Network.AWS.S3            as S3
-import           Network.AWS.Types         as AWS
 
 import           Text.RawString.QQ
 
@@ -137,55 +137,62 @@
       , _srSns =
         SNSMessage
         { _smMessage =
-          Embedded
-          { _unEmbed =
-            RecordsEvent
-            { _reRecords =
-              [ S3EventNotification
-                { _senAwsRegion = Sydney
-                , _senEventName = S3ObjectCreatedPut
-                , _senEventSource = "aws:s3"
-                , _senEventTime = UTCTime (fromGregorian 2017 3 6) (picosecondsToDiffTime 10579713000000000)
-                , _senEventVersion = "2.0"
-                , _senRequestParameters =
-                  RequestParametersEntity
-                  { _rpeSourceIPAddress = "192.168.0.1"
-                  }
-                , _senResponseElements =
-                  ResponseElementsEntity
-                  { _reeXAmzId2 =
-                    "xsdSDF/pgAl401Fz3UIATJ5/didfljDSFDSFsdfkjsdfl8JdsfLSDF89ldsf7SDF898jsdfljiA="
-                  , _reeXAmzRequestId = "324098EDFLK0894F"
-                  }
-                , _senS3 =
-                  S3Entity
-                  { _seBucket =
-                    S3BucketEntity
-                    { _sbeArn = "arn:aws:s3:::some-bucket"
-                    , _sbeName = BucketName "some-bucket"
-                    , _sbeOwnerIdentity =
-                      UserIdentityEntity
-                      { _uiePrincipalId = "A3O1SDFLKJIJXU"
+          TextValue
+          { _unTextValue =
+            Embedded
+            { _unEmbed =
+              RecordsEvent
+              { _reRecords =
+                [ S3EventNotification
+                  { _senAwsRegion = Sydney
+                  , _senEventName = S3ObjectCreatedPut
+                  , _senEventSource = "aws:s3"
+                  , _senEventTime =
+                    UTCTime
+                      (fromGregorian 2017 3 6)
+                      (picosecondsToDiffTime 10579713000000000)
+                  , _senEventVersion = "2.0"
+                  , _senRequestParameters =
+                    RequestParametersEntity
+                    { _rpeSourceIPAddress = "192.168.0.1"
+                    }
+                  , _senResponseElements =
+                    ResponseElementsEntity
+                    { _reeXAmzId2 =
+                      "xsdSDF/pgAl401Fz3UIATJ5/didfljDSFDSFsdfkjsdfl8JdsfLSDF89ldsf7SDF898jsdfljiA="
+                    , _reeXAmzRequestId = "324098EDFLK0894F"
+                    }
+                  , _senS3 =
+                    S3Entity
+                    { _seBucket =
+                      S3BucketEntity
+                      { _sbeArn = "arn:aws:s3:::some-bucket"
+                      , _sbeName = BucketName "some-bucket"
+                      , _sbeOwnerIdentity =
+                        UserIdentityEntity
+                        { _uiePrincipalId = "A3O1SDFLKJIJXU"
+                        }
                       }
+                    , _seConfigurationId = "SomeS3Event:Created"
+                    , _seObject =
+                      S3ObjectEntity
+                      { _soeETag =
+                        Just (ETag "6b1f72b9e81e4d6fcd3e0c808e8477f8")
+                      , _soeKey = ObjectKey "path/to/some/object"
+                      , _soeSize = Just 53598442
+                      , _soeSequencer = "0058BCCFD25C798E7B"
+                      , _soeVersionId = Nothing
+                      }
+                    , _seS3SchemaVersion = "1.0"
                     }
-                  , _seConfigurationId = "SomeS3Event:Created"
-                  , _seObject =
-                    S3ObjectEntity
-                    { _soeETag = Just (ETag "6b1f72b9e81e4d6fcd3e0c808e8477f8")
-                    , _soeKey = ObjectKey "path/to/some/object"
-                    , _soeSize = Just 53598442
-                    , _soeSequencer = "0058BCCFD25C798E7B"
-                    , _soeVersionId = Nothing
+                  , _senUserIdentity =
+                    UserIdentityEntity
+                    { _uiePrincipalId =
+                      "AWS:DFLKSDFLKJ987SDFLJJDJ:some-principal-id"
                     }
-                  , _seS3SchemaVersion = "1.0"
                   }
-                , _senUserIdentity =
-                  UserIdentityEntity
-                  { _uiePrincipalId =
-                    "AWS:DFLKSDFLKJ987SDFLJJDJ:some-principal-id"
-                  }
-                }
-              ]
+                ]
+              }
             }
           }
         , _smMessageAttributes = HashMap.fromList []
@@ -196,7 +203,10 @@
         , _smSigningCertUrl =
           "https://sns.ap-southeast-2.amazonaws.com/SimpleNotificationService-b95095beb82e8f6a046b3aafc7f4149a.pem"
         , _smSubject = "Amazon S3 Notification"
-        , _smTimestamp = UTCTime (fromGregorian 2017 3 6) (picosecondsToDiffTime 10579834000000000)
+        , _smTimestamp =
+          UTCTime
+            (fromGregorian 2017 3 6)
+            (picosecondsToDiffTime 10579834000000000)
         , _smTopicArn = "arn:aws:sns:ap-southeast-2:012345678901:SomeSNSEvent"
         , _smType = "Notification"
         , _smUnsubscribeUrl =
