diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,3 @@
+# Changelog for plaid
+
+## Unreleased changes
diff --git a/Data/Api/Accounts.hs b/Data/Api/Accounts.hs
new file mode 100644
--- /dev/null
+++ b/Data/Api/Accounts.hs
@@ -0,0 +1,27 @@
+-- |
+--
+-- Module      :  Data.Api.Product.Accounts
+-- Copyright   :  2019 Sasha Bogicevic
+-- License     :  BSD 3 clause
+--
+-- Maintainer  :  Sasha Bogicevic <sasa.bogicevic@pm.me>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+module Data.Api.Accounts
+  ( plaidGetBalance
+  ) where
+
+import           Data.Api.Types (executePost)
+import           Data.Common
+
+-- | Get the account balance
+-- http://plaid.com/docs/#retrieve-balance-request
+plaidGetBalance
+  :: ( MonadReader PlaidEnv m
+     , PlaidHttp m)
+  => PlaidBody GetBalance
+  -> m ByteString
+plaidGetBalance body = do
+  env <- ask
+  executePost (envUrl (env ^. plaidEnvEnvironment) <> "/accounts/balance/get") body
diff --git a/Data/Api/Auth.hs b/Data/Api/Auth.hs
new file mode 100644
--- /dev/null
+++ b/Data/Api/Auth.hs
@@ -0,0 +1,17 @@
+module Data.Api.Auth
+  ( plaidGetAuth
+  ) where
+
+import           Data.Api.Types (executePost)
+import           Data.Common
+
+plaidGetAuth
+  :: ( MonadReader PlaidEnv m
+     , MonadThrow m
+     , PlaidHttp m
+     )
+  => PlaidBody AuthGet
+  -> m ByteString
+plaidGetAuth body = do
+  env <- ask
+  executePost (envUrl (env ^. plaidEnvEnvironment) <> "/auth/get") body
diff --git a/Data/Api/Helper.hs b/Data/Api/Helper.hs
new file mode 100644
--- /dev/null
+++ b/Data/Api/Helper.hs
@@ -0,0 +1,26 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Data.Api.Helper
+  ( parseOrFail
+  ) where
+
+
+import           Control.Exception.Safe (MonadThrow, throwM)
+import           Data.Aeson (FromJSON, eitherDecode)
+import           Data.Api.Types
+import qualified Data.ByteString.Lazy as BSL
+import qualified Data.Text as T
+
+-- | Try to parse any plaid response to appropriate type or fail
+parseOrFail
+  :: forall b m.
+     ( MonadThrow m
+     , PlaidHttp m
+     , FromJSON b)
+  => BSL.ByteString
+  -> m b
+parseOrFail bs = do
+  let edecoded = eitherDecode bs :: Either String b
+  case edecoded of
+    Left e        -> throwM (PlaidError $ T.pack e)
+    Right decoded -> return decoded
diff --git a/Data/Api/Identity.hs b/Data/Api/Identity.hs
new file mode 100644
--- /dev/null
+++ b/Data/Api/Identity.hs
@@ -0,0 +1,17 @@
+module Data.Api.Identity
+  ( plaidGetIdentity
+  ) where
+
+import           Data.Common
+
+plaidGetIdentity
+  :: ( MonadReader PlaidEnv m
+     , MonadThrow m
+     , PlaidHttp m
+     )
+  => PlaidBody PlaidIdentityGet
+  -> m ByteString
+plaidGetIdentity body = do
+  env <- ask
+  let url = envUrl (env ^. plaidEnvEnvironment)
+  executePost (url <> "/identity/get") body
diff --git a/Data/Api/Income.hs b/Data/Api/Income.hs
new file mode 100644
--- /dev/null
+++ b/Data/Api/Income.hs
@@ -0,0 +1,17 @@
+module Data.Api.Income
+  ( plaidGetIncome
+  ) where
+
+import           Data.Common
+
+plaidGetIncome
+  :: ( MonadReader PlaidEnv m
+     , MonadThrow m
+     , PlaidHttp m
+     )
+  => PlaidBody PlaidIncomeGet
+  -> m ByteString
+plaidGetIncome body = do
+  env <- ask
+  let url = envUrl (env ^. plaidEnvEnvironment)
+  executePost (url <> "/income/get") body
diff --git a/Data/Api/InternalPure.hs b/Data/Api/InternalPure.hs
new file mode 100644
--- /dev/null
+++ b/Data/Api/InternalPure.hs
@@ -0,0 +1,131 @@
+module Data.Api.InternalPure
+  ( mkExchangePublicTokenEnv
+  , mkCreatePublicTokenEnv
+  , mkGetBalanceEnv
+  , mkGetAuthEnv
+  , mkCreateTransactionsGetEnv
+  , mkCreateIdentityGetEnv
+  , mkCreateIncomeGetEnv
+  ) where
+
+import           Data.Common
+import           Data.Proof.Proof
+import           Data.Time        (Day)
+
+mkGetAuthEnv
+  :: PlaidEnv
+  -> AccessToken
+  -> Either PlaidError (PlaidBody AuthGet)
+mkGetAuthEnv env atoken =
+  name env (\namedEnv ->
+    name atoken (\namedAccessToken ->
+      let proof = proveAuthGetBody namedEnv namedAccessToken
+          body = mempty & plaidBodyEnv .~ env
+                        & plaidBodyAccessToken ?~ atoken
+      in either (Left . PlaidError) (const (Right body)) proof
+  ))
+
+mkGetBalanceEnv
+  :: PlaidEnv
+  -> AccessToken
+  -> Either PlaidError (PlaidBody GetBalance)
+mkGetBalanceEnv env atoken =
+  name env (\namedEnv ->
+    name atoken (\namedAccessToken ->
+      let proof = proveGetBalanceBody namedEnv namedAccessToken
+          body = mempty & plaidBodyEnv .~ env
+                        & plaidBodyAccessToken ?~ atoken
+       in either (Left . PlaidError) (const (return body)) proof
+  ))
+
+-- | Creates a Public Token
+-- https://plaid.com/docs/#exchange-token-flow
+mkCreatePublicTokenEnv
+  :: PlaidEnv
+  -> InstitutionId
+  -> [PlaidProduct]
+  -> Maybe PlaidOptions
+  -> Either PlaidError (PlaidBody PublicTokenCreate)
+mkCreatePublicTokenEnv env instId products options =
+  name env (\namedEnv ->
+    name instId (\namedInstId ->
+      name products (\namedProducts ->
+        name options (\namedOptions ->
+          let proof = proveCreatePublicTokenBody namedEnv namedInstId namedProducts namedOptions
+              body = mempty & plaidBodyEnv .~ env
+                            & plaidBodyOptions .~ options
+                            & plaidBodyInstitutionId ?~ instId
+                            & plaidBodyInitialProducts ?~ products
+           in either (Left . PlaidError) (const (Right body)) proof
+  ))))
+
+mkExchangePublicTokenEnv
+  :: PlaidEnv
+  -> PublicToken
+  -> Either PlaidError (PlaidBody PlaidTokenExchange)
+mkExchangePublicTokenEnv _ (PublicToken "") = Left $ PlaidError "PublicToken cannot be empty"
+mkExchangePublicTokenEnv env publicToken =
+  Right $ mempty & plaidBodyEnv .~ env
+                 & plaidBodyPublicToken ?~ publicToken
+
+
+-- | Creates request body for /transactions/get call
+-- https://plaid.com/docs/#retrieve-transactions-request
+mkCreateTransactionsGetEnv
+  :: PlaidEnv
+  -> AccessToken
+  -> Day
+  -> Day
+  -> Maybe PlaidOptions
+  -> Maybe PlaidPaginationOptions
+  -> Either PlaidError (PlaidBody PlaidTransactionsGet)
+mkCreateTransactionsGetEnv env accessToken startDate endDate options paginationOptions =
+  name env (\namedEnv ->
+    name accessToken (\namedAccessToken ->
+      name startDate (\namedStartDate ->
+        name endDate (\namedEndDate ->
+          name options (\namedOptions ->
+            name paginationOptions (\namedPaginationOptions ->
+              let proof =
+                    proveTransactionsGetBody namedEnv namedAccessToken namedStartDate namedEndDate namedOptions namedPaginationOptions
+                  body = mempty & plaidBodyEnv .~ env
+                                & plaidBodyAccessToken ?~ accessToken
+                                & plaidBodyOptions .~ options
+                                & plaidBodyStartDate ?~ startDate
+                                & plaidBodyEndDate ?~ endDate
+                                & plaidBodyPaginationOptions ?~ defPaginationOptions
+
+              in either (Left . PlaidError) (const (Right body)) proof
+  ))))))
+
+-- | Creates request body for /identity/get call
+-- https://plaid.com/docs/#identity
+mkCreateIdentityGetEnv
+  :: PlaidEnv
+  -> AccessToken
+  -> Either PlaidError (PlaidBody PlaidIdentityGet)
+mkCreateIdentityGetEnv env accessToken =
+  name env (\namedEnv ->
+    name accessToken (\namedAccessToken ->
+      let proof = proveIdentityGetBody namedEnv namedAccessToken
+          body = mempty & plaidBodyEnv .~ env
+                        & plaidBodyAccessToken ?~ accessToken
+
+      in either (Left . PlaidError) (const (Right body)) proof
+  ))
+
+-- | Creates request body for /income/get call
+-- https://plaid.com/docs/#income
+mkCreateIncomeGetEnv
+  :: PlaidEnv
+  -> AccessToken
+  -> Either PlaidError (PlaidBody PlaidIncomeGet)
+mkCreateIncomeGetEnv env accessToken =
+  name env (\namedEnv ->
+    name accessToken (\namedAccessToken ->
+      let proof = proveIncomeGetBody namedEnv namedAccessToken
+          body = mempty & plaidBodyEnv .~ env
+                        & plaidBodyAccessToken ?~ accessToken
+
+      in either (Left . PlaidError) (const (Right body)) proof
+  ))
diff --git a/Data/Api/Link.hs b/Data/Api/Link.hs
new file mode 100644
--- /dev/null
+++ b/Data/Api/Link.hs
@@ -0,0 +1,48 @@
+-- |
+--
+-- Module      :  Data.Api.Link.Link
+-- Copyright   :  2019 Sasha Bogicevic
+-- License     :  BSD 3 clause
+--
+-- Maintainer  :  Sasha Bogicevic <sasa.bogicevic@pm.me>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+module Data.Api.Link
+  ( plaidCreatePublicToken
+  , plaidExchangeToken
+  ) where
+
+import           Data.Api.Types (executePost)
+import           Data.Common
+
+-- | Creates a public token
+-- that can be used in further interaction with plaid.com
+-- https://plaid.com/docs/#creating-public-tokens
+-- returns PlaidPublicTokenResponse or fails with PlaidError
+plaidCreatePublicToken
+  :: ( MonadReader PlaidEnv m
+     , MonadThrow m
+     , PlaidHttp m
+     )
+  => PlaidBody PublicTokenCreate
+  -> m PlaidPublicTokenResponse
+plaidCreatePublicToken body = do
+  env <- ask
+  response <- executePost (envUrl (env ^. plaidEnvEnvironment) <> "/sandbox/public_token/create") body
+  parseOrFail response
+
+-- | Exchange a public token to access token.
+-- Public token is invalidated after the exchange.
+-- https://plaid.com/docs/#exchange-token-flow
+-- returns PlaidAccessTokenResponse or fails with PlaidError
+plaidExchangeToken
+  :: ( MonadReader PlaidEnv m
+     , MonadThrow m
+     , PlaidHttp m)
+  => PlaidBody PlaidTokenExchange
+  -> m PlaidAccessTokenResponse
+plaidExchangeToken body = do
+  env <- ask
+  response <- executePost (envUrl (env ^. plaidEnvEnvironment) <> "/item/public_token/exchange") body
+  parseOrFail response
diff --git a/Data/Api/Plaid.hs b/Data/Api/Plaid.hs
new file mode 100644
--- /dev/null
+++ b/Data/Api/Plaid.hs
@@ -0,0 +1,26 @@
+module Data.Api.Plaid
+  ( mkExchangePublicTokenEnv
+  , mkCreatePublicTokenEnv
+  , mkGetBalanceEnv
+  , mkGetAuthEnv
+  , mkCreateTransactionsGetEnv
+  , mkCreateIdentityGetEnv
+  , mkCreateIncomeGetEnv
+  , plaidCreatePublicToken
+  , plaidExchangeToken
+  , plaidGetAuth
+  , plaidGetBalance
+  , plaidGetTransactions
+  , plaidGetIdentity
+  , plaidGetIncome
+  ) where
+
+import           Data.Api.Accounts     (plaidGetBalance)
+import           Data.Api.Auth         (plaidGetAuth)
+import           Data.Api.Identity     (plaidGetIdentity)
+import           Data.Api.Income       (plaidGetIncome)
+import           Data.Api.InternalPure (mkCreateIdentityGetEnv, mkCreateIncomeGetEnv,
+                                        mkCreatePublicTokenEnv, mkCreateTransactionsGetEnv,
+                                        mkExchangePublicTokenEnv, mkGetAuthEnv, mkGetBalanceEnv)
+import           Data.Api.Link         (plaidCreatePublicToken, plaidExchangeToken)
+import           Data.Api.Transactions (plaidGetTransactions)
diff --git a/Data/Api/TestByteStrings.hs b/Data/Api/TestByteStrings.hs
new file mode 100644
--- /dev/null
+++ b/Data/Api/TestByteStrings.hs
@@ -0,0 +1,880 @@
+{-# LANGUAGE QuasiQuotes #-}
+
+module Data.Api.TestByteStrings where
+
+import           Data.ByteString.Lazy (ByteString)
+import           Text.RawString.QQ    (r)
+
+responseAuthGet :: ByteString
+responseAuthGet = [r|{"accounts":[{"account_id":"vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D","balances":{"available":100,"current":110,"limit":null,"iso_currency_code":"USD","unofficial_currency_code":null},"mask":"9606","name":"Plaid Checking","official_name":"Plaid Gold Checking","subtype":"checking","type":"depository"}],"numbers":{"ach":[{"account":"9900009606","account_id":"vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D","routing":"011401533","wire_routing":"021000021"}],"eft":[{"account":"111122223333","account_id":"vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D","institution":"021","branch":"01140"}],"international":[{"account_id":"vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D","bic":"NWBKGB21","iban":"GB29NWBK60161331926819"}],"bacs":[{"account":"31926819","account_id":"vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D","sort_code":"601613"}]},"item":{"available_products":["assets","balance","credit_details","identity","income","investments","liabilities"],"billed_products":["auth","transactions"],"error": null,"institution_id":"ins_3","item_id":"MylwooVBojuBwgzNVdZ6FDLRnElJN3C9WPd49","webhook":"http://bogicevicsasa.com"},"request_id":"m8MDnv9okwxFNBV"} |]
+
+responseTransactionsGet :: ByteString
+responseTransactionsGet = [r|{"accounts":[{"account_id":"vokyE5Rn6vHKqDLRXEn5fne7LwbKPLIXGK98d","balances":{"available":100,"current":110,"limit":null,"iso_currency_code":"USD","unofficial_currency_code":null},"mask":"9606","name":"Plaid Checking","official_name":"Plaid Gold Checking","subtype":"checking","type":"depository"}], "transactions": [{"account_id": "vokyE5Rn6vHKqDLRXEn5fne7LwbKPLIXGK98d", "amount": 2307.21, "iso_currency_code": "USD", "unofficial_currency_code": null, "category": ["Shops", "Computers and Electronics"], "category_id": "19013000", "date": "2017-01-29", "location": {"address": "300 Post St", "city": "San Francisco", "region": "CA", "postal_code": "94108", "country": "US", "lat": null, "lon": null}, "name": "Apple Store", "payment_meta": { "reference_number":null, "ppd_id":null, "payee":null}, "pending": false, "pending_transaction_id": null, "account_owner": null, "transaction_id": "lPNjeW1nR6CDn5okmGQ6hEpMo4lLNoSrzqDje", "transaction_type": "place"}, {"account_id": "XA96y1wW3xS7wKyEdbRzFkpZov6x1ohxMXwep", "amount": 78.5, "iso_currency_code": "USD", "unofficial_currency_code": null, "category": ["Food and Drink", "Restaurants"], "category_id": "13005000", "date": "2017-01-29", "location": {"address": "262 W 15th St", "city": "New York", "region": "NY", "postal_code": "10011", "country": "US", "lat": 40.740352, "lon": -74.001761}, "name": "Golden Crepes", "payment_meta": { "reference_number":null, "ppd_id":null, "payee":null}, "pending": false, "pending_transaction_id": null, "account_owner": null, "transaction_id": "4WPD9vV5A1cogJwyQ5kVFB3vPEmpXPS3qvjXQ", "transaction_type": "place"}], "item": {"available_products":["assets","balance","credit_details","identity","income","investments","liabilities"],"billed_products":["auth","transactions"],"error": null,"institution_id":"ins_3","item_id":"MylwooVBojuBwgzNVdZ6jFDLRnElJN3C9WPd49","webhook":"http://bogicevicsasa.com"}, "total_transactions": 100, "request_id": "45QSn"}|]
+
+responsePublicTokenCreate :: ByteString
+responsePublicTokenCreate = [r|
+   {"public_token":"public-sandbox-b0e2c4ee-a763-4df5-bfe9-46a46bce993d","request_id":"Aim3b"}
+|]
+
+responsePublicTokenExchange :: ByteString
+responsePublicTokenExchange = [r|
+  {"access_token":"access-sandbox-de3ce8ef-33f8-452c-a685-8671031fc0f6","item_id":"M5eVJqLnv3tbzdngLDp9FL5OlDNxlNhlE55op","request_id":"Aim3b"}
+|]
+
+balanceJson :: ByteString
+balanceJson = [r|{"available":100,"current":110,"limit":null,"iso_currency_code":"USD","unofficial_currency_code":null}|]
+
+accountJson :: ByteString
+accountJson = [r|{"account_id":"vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D","balances":{"available":100,"current":110,"limit":null,"iso_currency_code":"USD","unofficial_currency_code":null},"mask":"9606","name":"Plaid Checking","official_name":"Plaid Gold Checking","subtype":"checking","type":"depository"}|]
+
+accountListJson :: ByteString
+accountListJson = [r|[{"account_id":"vokyE5Rn6vHKqDLRXEn5fne7LwbKPLIXGK98d","balances":{"available":100,"current":110,"limit":null,"iso_currency_code":"USD","unofficial_currency_code":null},"mask":"9606","name":"Plaid Checking","official_name":"Plaid Gold Checking","subtype":"checking","type":"depository"}]|]
+
+paymentMetaJson :: ByteString
+paymentMetaJson = [r|{"reference_number":null, "ppd_id":null, "payee":null}|]
+
+itemJson :: ByteString
+itemJson = [r|{"available_products": ["assets","balance","credit_details","identity","income","investments","liabilities"], "billed_products":["auth","transactions"], "error": null, "institution_id":"ins_3", "item_id":"MylwooVBojuBwgzNVdZ6jFDLRnElJN3C9WPd49", "webhook":"http://bogicevicsasa.com"}|]
+
+transactionJson :: ByteString
+transactionJson = [r|{"account_id": "vokyE5Rn6vHKqDLRXEn5fne7LwbKPLIXGK98d", "amount": 2307.21, "iso_currency_code": "USD", "unofficial_currency_code": null, "category": ["Shops", "Computers and Electronics"], "category_id": "19013000", "date": "2017-01-29", "location": {"address": "300 Post St", "city": "San Francisco", "region": "CA", "postal_code": "94108", "country": "US", "lat": null, "lon": null}, "name": "Apple Store", "payment_meta": { "reference_number":null, "ppd_id":null, "payee":null}, "pending": false, "pending_transaction_id": null, "account_owner": null, "transaction_id": "lPNjeW1nR6CDn5okmGQ6hEpMo4lLNoSrzqDje", "transaction_type": "place"}|]
+
+transactionLocationJson :: ByteString
+transactionLocationJson = [r|{"address": "300 Post St", "city": "San Francisco", "region": "CA", "postal_code": "94108", "country": "US", "lat": null, "lon": null}|]
+
+emailJson :: ByteString
+emailJson = [r|
+  {
+    "data": "accountholder0@example.com",
+    "primary": true,
+    "type": "primary"
+  }
+|]
+
+phoneNumberJson :: ByteString
+phoneNumberJson = [r|
+  {
+    "data": "1112223333",
+    "primary": false,
+    "type": "home"
+  }
+|]
+
+addressesJson :: ByteString
+addressesJson = [r|
+  {
+    "data": {
+      "city": "Malakoff",
+      "country": null,
+      "postal_code": "14236",
+      "region": "NY",
+      "street": "2992 Cameron Road"
+    },
+    "primary": true
+  }
+|]
+
+ownersJson :: ByteString
+ownersJson = [r|
+  {
+    "addresses": [
+      {
+        "data": {
+          "city": "Malakoff",
+          "country": null,
+          "postal_code": "14236",
+          "region": "NY",
+          "street": "2992 Cameron Road"
+        },
+        "primary": true
+      },
+      {
+        "data": {
+          "city": "San Matias",
+          "country": null,
+          "postal_code": "93405-2255",
+          "region": "CA",
+          "street": "2493 Leisure Lane"
+        },
+        "primary": false
+      }
+    ],
+    "emails": [
+      {
+        "data": "accountholder0@example.com",
+        "primary": true,
+        "type": "primary"
+      },
+      {
+        "data": "accountholder1@example.com",
+        "primary": false,
+        "type": "secondary"
+      },
+      {
+        "data": "extraordinarily.long.email.username.123",
+        "primary": false,
+        "type": "other"
+      }
+    ],
+    "names": [
+      "Alberta Bobbeth Charleson"
+    ],
+    "phone_numbers": [
+      {
+        "data": "1112223333",
+        "primary": false,
+        "type": "home"
+      },
+      {
+        "data": "1112224444",
+        "primary": false,
+        "type": "work"
+      },
+      {
+        "data": "1112225555",
+        "primary": false,
+        "type": "mobile1"
+      }
+    ]
+  }
+|]
+
+accountsJson :: ByteString
+accountsJson = [r|
+{
+     "account_id": "9xPwwnJGpDI8Qby6ElmEuk1jGDmjlqcRWwLAz",
+     "balances": {
+       "available": 100,
+       "current": 110,
+       "iso_currency_code": "USD",
+       "limit": null,
+       "unofficial_currency_code": null
+     },
+     "mask": "0000",
+     "name": "Plaid Checking",
+     "official_name": "Plaid Gold Standard 0% Interest Checki",
+     "owners": [
+       {
+         "addresses": [
+           {
+             "data": {
+               "city": "Malakoff",
+               "country": null,
+               "postal_code": "14236",
+               "region": "NY",
+               "street": "2992 Cameron Road"
+             },
+             "primary": true
+           },
+           {
+             "data": {
+               "city": "San Matias",
+               "country": null,
+               "postal_code": "93405-2255",
+               "region": "CA",
+               "street": "2493 Leisure Lane"
+             },
+             "primary": false
+           }
+         ],
+         "emails": [
+           {
+             "data": "accountholder0@example.com",
+             "primary": true,
+             "type": "primary"
+           },
+           {
+             "data": "accountholder1@example.com",
+             "primary": false,
+             "type": "secondary"
+           },
+           {
+             "data": "extraordinarily.long.email.username.123",
+             "primary": false,
+             "type": "other"
+           }
+         ],
+         "names": [
+           "Alberta Bobbeth Charleson"
+         ],
+         "phone_numbers": [
+           {
+             "data": "1112223333",
+             "primary": false,
+             "type": "home"
+           },
+           {
+             "data": "1112224444",
+             "primary": false,
+             "type": "work"
+           },
+           {
+             "data": "1112225555",
+             "primary": false,
+             "type": "mobile1"
+           }
+         ]
+       }
+     ],
+     "subtype": "checking",
+     "type": "depository"
+   }
+|]
+
+identityJson :: ByteString
+identityJson = [r|
+{
+ "accounts": [
+   {
+     "account_id": "9xPwwnJGpDI8Qby6ElmEuk1jGDmjlqcRWwLAz",
+     "balances": {
+       "available": 100,
+       "current": 110,
+       "iso_currency_code": "USD",
+       "limit": null,
+       "unofficial_currency_code": null
+     },
+     "mask": "0000",
+     "name": "Plaid Checking",
+     "official_name": "Plaid Gold Standard 0% Interest Checki",
+     "owners": [
+       {
+         "addresses": [
+           {
+             "data": {
+               "city": "Malakoff",
+               "country": null,
+               "postal_code": "14236",
+               "region": "NY",
+               "street": "2992 Cameron Road"
+             },
+             "primary": true
+           },
+           {
+             "data": {
+               "city": "San Matias",
+               "country": null,
+               "postal_code": "93405-2255",
+               "region": "CA",
+               "street": "2493 Leisure Lane"
+             },
+             "primary": false
+           }
+         ],
+         "emails": [
+           {
+             "data": "accountholder0@example.com",
+             "primary": true,
+             "type": "primary"
+           },
+           {
+             "data": "accountholder1@example.com",
+             "primary": false,
+             "type": "secondary"
+           },
+           {
+             "data": "extraordinarily.long.email.username.123",
+             "primary": false,
+             "type": "other"
+           }
+         ],
+         "names": [
+           "Alberta Bobbeth Charleson"
+         ],
+         "phone_numbers": [
+           {
+             "data": "1112223333",
+             "primary": false,
+             "type": "home"
+           },
+           {
+             "data": "1112224444",
+             "primary": false,
+             "type": "work"
+           },
+           {
+             "data": "1112225555",
+             "primary": false,
+             "type": "mobile1"
+           }
+         ]
+       }
+     ],
+     "subtype": "checking",
+     "type": "depository"
+   },
+   {
+     "account_id": "v4xRRVvpzyTlBMpaDjQDFWAGVXNGadSW3BjNo",
+     "balances": {
+       "available": 200,
+       "current": 210,
+       "iso_currency_code": "USD",
+       "limit": null,
+       "unofficial_currency_code": null
+     },
+     "mask": "1111",
+     "name": "Plaid Saving",
+     "official_name": "Plaid Silver Standard 0.1% Interest Sa",
+     "owners": [
+       {
+         "addresses": [
+           {
+             "data": {
+               "city": "Malakoff",
+               "country": null,
+               "postal_code": "14236",
+               "region": "NY",
+               "street": "2992 Cameron Road"
+             },
+             "primary": true
+           },
+           {
+             "data": {
+               "city": "San Matias",
+               "country": null,
+               "postal_code": "93405-2255",
+               "region": "CA",
+               "street": "2493 Leisure Lane"
+             },
+             "primary": false
+           }
+         ],
+         "emails": [
+           {
+             "data": "accountholder0@example.com",
+             "primary": true,
+             "type": "primary"
+           },
+           {
+             "data": "accountholder1@example.com",
+             "primary": false,
+             "type": "secondary"
+           },
+           {
+             "data": "extraordinarily.long.email.username.123",
+             "primary": false,
+             "type": "other"
+           }
+         ],
+         "names": [
+           "Alberta Bobbeth Charleson"
+         ],
+         "phone_numbers": [
+           {
+             "data": "1112223333",
+             "primary": false,
+             "type": "home"
+           },
+           {
+             "data": "1112224444",
+             "primary": false,
+             "type": "work"
+           },
+           {
+             "data": "1112225555",
+             "primary": false,
+             "type": "mobile1"
+           }
+         ]
+       }
+     ],
+     "subtype": "savings",
+     "type": "depository"
+   },
+   {
+     "account_id": "Rrz11gqRL6SVljzWGJkGI7apezqpJbhRvWKDR",
+     "balances": {
+       "available": null,
+       "current": 1000,
+       "iso_currency_code": "USD",
+       "limit": null,
+       "unofficial_currency_code": null
+     },
+     "mask": "2222",
+     "name": "Plaid CD",
+     "official_name": "Plaid Bronze Standard 0.2% Interest CD",
+     "owners": [
+       {
+         "addresses": [
+           {
+             "data": {
+               "city": "Malakoff",
+               "country": null,
+               "postal_code": "14236",
+               "region": "NY",
+               "street": "2992 Cameron Road"
+             },
+             "primary": true
+           },
+           {
+             "data": {
+               "city": "San Matias",
+               "country": null,
+               "postal_code": "93405-2255",
+               "region": "CA",
+               "street": "2493 Leisure Lane"
+             },
+             "primary": false
+           }
+         ],
+         "emails": [
+           {
+             "data": "accountholder0@example.com",
+             "primary": true,
+             "type": "primary"
+           },
+           {
+             "data": "accountholder1@example.com",
+             "primary": false,
+             "type": "secondary"
+           },
+           {
+             "data": "extraordinarily.long.email.username.123",
+             "primary": false,
+             "type": "other"
+           }
+         ],
+         "names": [
+           "Alberta Bobbeth Charleson"
+         ],
+         "phone_numbers": [
+           {
+             "data": "1112223333",
+             "primary": false,
+             "type": "home"
+           },
+           {
+             "data": "1112224444",
+             "primary": false,
+             "type": "work"
+           },
+           {
+             "data": "1112225555",
+             "primary": false,
+             "type": "mobile1"
+           }
+         ]
+       }
+     ],
+     "subtype": "cd",
+     "type": "depository"
+   },
+   {
+     "account_id": "6zaBBWpoM5HJqEkVy5zyUeML9xRL8kigopWN8",
+     "balances": {
+       "available": null,
+       "current": 410,
+       "iso_currency_code": "USD",
+       "limit": 2000,
+       "unofficial_currency_code": null
+     },
+     "mask": "3333",
+     "name": "Plaid Credit Card",
+     "official_name": "Plaid Diamond 12.5% APR Interest Credi",
+     "owners": [
+       {
+         "addresses": [
+           {
+             "data": {
+               "city": "Malakoff",
+               "country": null,
+               "postal_code": "14236",
+               "region": "NY",
+               "street": "2992 Cameron Road"
+             },
+             "primary": true
+           },
+           {
+             "data": {
+               "city": "San Matias",
+               "country": null,
+               "postal_code": "93405-2255",
+               "region": "CA",
+               "street": "2493 Leisure Lane"
+             },
+             "primary": false
+           }
+         ],
+         "emails": [
+           {
+             "data": "accountholder0@example.com",
+             "primary": true,
+             "type": "primary"
+           },
+           {
+             "data": "accountholder1@example.com",
+             "primary": false,
+             "type": "secondary"
+           },
+           {
+             "data": "extraordinarily.long.email.username.123",
+             "primary": false,
+             "type": "other"
+           }
+         ],
+         "names": [
+           "Alberta Bobbeth Charleson"
+         ],
+         "phone_numbers": [
+           {
+             "data": "1112223333",
+             "primary": false,
+             "type": "home"
+           },
+           {
+             "data": "1112224444",
+             "primary": false,
+             "type": "work"
+           },
+           {
+             "data": "1112225555",
+             "primary": false,
+             "type": "mobile1"
+           }
+         ]
+       }
+     ],
+     "subtype": "credit card",
+     "type": "credit"
+   },
+   {
+     "account_id": "Xa4wwnxzl6IQWD4eKBvKFwjeG4XeBLHdWN4V1",
+     "balances": {
+       "available": 43200,
+       "current": 43200,
+       "iso_currency_code": "USD",
+       "limit": null,
+       "unofficial_currency_code": null
+     },
+     "mask": "4444",
+     "name": "Plaid Money Market",
+     "official_name": "Plaid Platinum Standard 1.85% Interest",
+     "owners": [
+       {
+         "addresses": [
+           {
+             "data": {
+               "city": "Malakoff",
+               "country": null,
+               "postal_code": "14236",
+               "region": "NY",
+               "street": "2992 Cameron Road"
+             },
+             "primary": true
+           },
+           {
+             "data": {
+               "city": "San Matias",
+               "country": null,
+               "postal_code": "93405-2255",
+               "region": "CA",
+               "street": "2493 Leisure Lane"
+             },
+             "primary": false
+           }
+         ],
+         "emails": [
+           {
+             "data": "accountholder0@example.com",
+             "primary": true,
+             "type": "primary"
+           },
+           {
+             "data": "accountholder1@example.com",
+             "primary": false,
+             "type": "secondary"
+           },
+           {
+             "data": "extraordinarily.long.email.username.123",
+             "primary": false,
+             "type": "other"
+           }
+         ],
+         "names": [
+           "Alberta Bobbeth Charleson"
+         ],
+         "phone_numbers": [
+           {
+             "data": "1112223333",
+             "primary": false,
+             "type": "home"
+           },
+           {
+             "data": "1112224444",
+             "primary": false,
+             "type": "work"
+           },
+           {
+             "data": "1112225555",
+             "primary": false,
+             "type": "mobile1"
+           }
+         ]
+       }
+     ],
+     "subtype": "money market",
+     "type": "depository"
+   },
+   {
+     "account_id": "DMgwwaJx36SdwzmkaNMaSlxb56PbNXtvPBgVE",
+     "balances": {
+       "available": null,
+       "current": 320.76,
+       "iso_currency_code": "USD",
+       "limit": null,
+       "unofficial_currency_code": null
+     },
+     "mask": "5555",
+     "name": "Plaid IRA",
+     "official_name": null,
+     "owners": [
+       {
+         "addresses": [
+           {
+             "data": {
+               "city": "Malakoff",
+               "country": null,
+               "postal_code": "14236",
+               "region": "NY",
+               "street": "2992 Cameron Road"
+             },
+             "primary": true
+           },
+           {
+             "data": {
+               "city": "San Matias",
+               "country": null,
+               "postal_code": "93405-2255",
+               "region": "CA",
+               "street": "2493 Leisure Lane"
+             },
+             "primary": false
+           }
+         ],
+         "emails": [
+           {
+             "data": "accountholder0@example.com",
+             "primary": true,
+             "type": "primary"
+           },
+           {
+             "data": "accountholder1@example.com",
+             "primary": false,
+             "type": "secondary"
+           },
+           {
+             "data": "extraordinarily.long.email.username.123",
+             "primary": false,
+             "type": "other"
+           }
+         ],
+         "names": [
+           "Alberta Bobbeth Charleson"
+         ],
+         "phone_numbers": [
+           {
+             "data": "1112223333",
+             "primary": false,
+             "type": "home"
+           },
+           {
+             "data": "1112224444",
+             "primary": false,
+             "type": "work"
+           },
+           {
+             "data": "1112225555",
+             "primary": false,
+             "type": "mobile1"
+           }
+         ]
+       }
+     ],
+     "subtype": "ira",
+     "type": "investment"
+   },
+   {
+     "account_id": "VxdggABl86IamVdJNXkNuMdEDnVEBWHW8X4xn",
+     "balances": {
+       "available": null,
+       "current": 23631.9805,
+       "iso_currency_code": "USD",
+       "limit": null,
+       "unofficial_currency_code": null
+     },
+     "mask": "6666",
+     "name": "Plaid 401k",
+     "official_name": null,
+     "owners": [
+       {
+         "addresses": [
+           {
+             "data": {
+               "city": "Malakoff",
+               "country": null,
+               "postal_code": "14236",
+               "region": "NY",
+               "street": "2992 Cameron Road"
+             },
+             "primary": true
+           },
+           {
+             "data": {
+               "city": "San Matias",
+               "country": null,
+               "postal_code": "93405-2255",
+               "region": "CA",
+               "street": "2493 Leisure Lane"
+             },
+             "primary": false
+           }
+         ],
+         "emails": [
+           {
+             "data": "accountholder0@example.com",
+             "primary": true,
+             "type": "primary"
+           },
+           {
+             "data": "accountholder1@example.com",
+             "primary": false,
+             "type": "secondary"
+           },
+           {
+             "data": "extraordinarily.long.email.username.123",
+             "primary": false,
+             "type": "other"
+           }
+         ],
+         "names": [
+           "Alberta Bobbeth Charleson"
+         ],
+         "phone_numbers": [
+           {
+             "data": "1112223333",
+             "primary": false,
+             "type": "home"
+           },
+           {
+             "data": "1112224444",
+             "primary": false,
+             "type": "work"
+           },
+           {
+             "data": "1112225555",
+             "primary": false,
+             "type": "mobile1"
+           }
+         ]
+       }
+     ],
+     "subtype": "401k",
+     "type": "investment"
+   },
+   {
+     "account_id": "wNd55VvgBwSRXJEP67k6Unl9wxJ9jvFr7xvkL",
+     "balances": {
+       "available": null,
+       "current": 65262,
+       "iso_currency_code": "USD",
+       "limit": null,
+       "unofficial_currency_code": null
+     },
+     "mask": "7777",
+     "name": "Plaid Student Loan",
+     "official_name": null,
+     "owners": [
+       {
+         "addresses": [
+           {
+             "data": {
+               "city": "Malakoff",
+               "country": null,
+               "postal_code": "14236",
+               "region": "NY",
+               "street": "2992 Cameron Road"
+             },
+             "primary": true
+           },
+           {
+             "data": {
+               "city": "San Matias",
+               "country": null,
+               "postal_code": "93405-2255",
+               "region": "CA",
+               "street": "2493 Leisure Lane"
+             },
+             "primary": false
+           }
+         ],
+         "emails": [
+           {
+             "data": "accountholder0@example.com",
+             "primary": true,
+             "type": "primary"
+           },
+           {
+             "data": "accountholder1@example.com",
+             "primary": false,
+             "type": "secondary"
+           },
+           {
+             "data": "extraordinarily.long.email.username.123",
+             "primary": false,
+             "type": "other"
+           }
+         ],
+         "names": [
+           "Alberta Bobbeth Charleson"
+         ],
+         "phone_numbers": [
+           {
+             "data": "1112223333",
+             "primary": false,
+             "type": "home"
+           },
+           {
+             "data": "1112224444",
+             "primary": false,
+             "type": "work"
+           },
+           {
+             "data": "1112225555",
+             "primary": false,
+             "type": "mobile1"
+           }
+         ]
+       }
+     ],
+     "subtype": "student",
+     "type": "loan"
+   }
+ ],
+ "item": {
+   "available_products": [
+     "assets",
+     "auth",
+     "balance",
+     "credit_details",
+     "income",
+     "investments",
+     "liabilities"
+   ],
+   "billed_products": [
+     "identity",
+     "transactions"
+   ],
+   "consent_expiration_time": null,
+   "error": null,
+   "institution_id": "ins_3",
+   "item_id": "Wda44G3KEVsz9ValeLmeh14nVAKLJwhlG8XN3",
+   "webhook": ""
+ },
+ "request_id": "Mjv0DF1S06RqIuT"
+}
+|]
+
+incomeJson :: ByteString
+incomeJson = [r|
+
+|]
diff --git a/Data/Api/Transactions.hs b/Data/Api/Transactions.hs
new file mode 100644
--- /dev/null
+++ b/Data/Api/Transactions.hs
@@ -0,0 +1,18 @@
+module Data.Api.Transactions
+  ( plaidGetTransactions
+  ) where
+
+import           Data.Common
+
+plaidGetTransactions
+  :: ( MonadReader PlaidEnv m
+     , MonadThrow m
+     , PlaidHttp m
+     )
+  => PlaidBody PlaidTransactionsGet
+  -> m ByteString
+plaidGetTransactions body = do
+  env <- ask
+  let url = envUrl (env ^. plaidEnvEnvironment)
+  executePost (url <> "/transactions/get") body
+
diff --git a/Data/Api/Types.hs b/Data/Api/Types.hs
new file mode 100644
--- /dev/null
+++ b/Data/Api/Types.hs
@@ -0,0 +1,769 @@
+{-# LANGUAGE DeriveAnyClass     #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE InstanceSigs       #-}
+
+module Data.Api.Types where
+
+import           Control.Exception.Safe
+import           Control.Monad.IO.Class   (MonadIO, liftIO)
+import           Control.Monad.Reader     (MonadReader (..), ReaderT (..))
+import           Data.Aeson
+import           Data.Aeson.TH
+import           Data.Api.TestByteStrings
+import           Data.ByteString.Lazy     (ByteString)
+import           Data.Fixed
+import           Data.Functor.Identity
+import qualified Data.Map                 as M
+import           Data.Maybe               (isJust, isNothing)
+import           Data.Monoid              ((<>))
+import           Data.Text                (Text)
+import qualified Data.Text                as T
+import           Data.Time                (Day)
+import           Lens.Micro
+import           Lens.Micro.TH
+import           Network.HTTP.Conduit
+import           Text.Casing              (camel, quietSnake)
+
+data Environment
+  = Sandbox
+  | Development
+  | Production deriving (Ord, Eq)
+
+$(deriveFromJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 11 }) ''Environment)
+
+instance Show Environment where
+  show Sandbox     = "sandbox"
+  show Development = "development"
+  show Production  = "production"
+
+class Monad m => PlaidHttp m where
+  executePost :: ToJSON a => Text -> a -> m ByteString
+  executeGet :: Text -> m ByteString
+
+newtype PlaidError =
+  PlaidError Text
+  deriving Show
+  deriving anyclass Exception
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 10 }) ''PlaidError)
+
+newtype AccessToken =
+  AccessToken
+    { unAccessToken :: Text
+    } deriving (Eq, Show)
+
+instance ToJSON AccessToken where
+  toJSON AccessToken {..} = String unAccessToken
+
+instance FromJSON AccessToken where
+  parseJSON = withText "AccessToken" $ \t ->
+    return $ AccessToken t
+
+newtype PublicToken =
+  PublicToken
+    { unPublicToken :: Text
+    } deriving (Eq, Show)
+
+instance ToJSON PublicToken where
+  toJSON PublicToken {..} = String unPublicToken
+
+instance FromJSON PublicToken where
+  parseJSON = withText "PublicToken" $ \t ->
+    return $ PublicToken t
+
+newtype ClientId =
+  ClientId
+    { unClientId :: Text
+    } deriving (Eq, Show)
+
+instance ToJSON ClientId where
+  toJSON ClientId {..} = String unClientId
+
+instance FromJSON ClientId where
+  parseJSON = withText "ClientId" $ \t ->
+    return $ ClientId t
+
+newtype Secret =
+  Secret
+    { unSecret :: Text
+    } deriving (Eq, Show)
+
+instance ToJSON Secret where
+  toJSON Secret {..} = String unSecret
+
+instance FromJSON Secret where
+  parseJSON = withText "Secret" $ \t ->
+    return $ Secret t
+
+newtype InstitutionId =
+  InstitutionId
+    { unInstitutionId :: Text
+    } deriving (Eq, Show)
+
+instance ToJSON InstitutionId where
+  toJSON InstitutionId {..} = String unInstitutionId
+
+instance FromJSON InstitutionId where
+  parseJSON = withText "InstitutionId" $ \t ->
+    return $ InstitutionId t
+
+newtype Url a = Url { unUrl :: Text }
+
+instance ToJSON (Url a) where
+  toJSON Url {..} = String unUrl
+
+instance FromJSON (Url a) where
+  parseJSON = withText "Url" $ \t ->
+    return $ Url t
+
+type PublicKey     = Text
+type AccessKey     = Text
+type PlaidProduct  = Text
+type Institution = Text
+type AccountNumber = Text
+type SortCode = Text
+type AccountId = Text
+type Iban = Text
+type Bic = Text
+type RequestId = Text
+
+data AuthGet
+data GetBalance
+data PublicTokenCreate
+data PlaidTokenExchange
+data PlaidTransactionsGet
+data PlaidIdentityGet
+data PlaidIncomeGet
+
+data PlaidOptions =
+  PlaidOptions
+    { _plaidOptionsWebHook          :: Text
+    , _plaidOptionsOverrideUsername :: Text
+    , _plaidOptionsOverridePassword :: Text
+    } deriving (Eq, Show)
+
+makeLenses ''PlaidOptions
+$(deriveFromJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 13 }) ''PlaidOptions)
+
+instance ToJSON PlaidOptions where
+  toJSON b =
+    object [ "webhook"           .= (b ^. plaidOptionsWebHook)
+           , "override_username" .= (b ^. plaidOptionsOverrideUsername)
+           , "override_password" .= (b ^. plaidOptionsOverridePassword)
+           ]
+
+data PlaidPaginationOptions =
+  PlaidPaginationOptions
+    { _plaidPaginationOptionsCount  :: Int
+    , _plaidPaginationOptionsOffset :: Int
+    } deriving (Eq, Show)
+
+makeLenses ''PlaidPaginationOptions
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 23 }) ''PlaidPaginationOptions)
+
+defPaginationOptions :: PlaidPaginationOptions
+defPaginationOptions =
+  PlaidPaginationOptions
+    { _plaidPaginationOptionsCount  = 100
+    , _plaidPaginationOptionsOffset = 0
+    }
+
+data PlaidEnv =
+  PlaidEnv
+    { _plaidEnvPublicKey   :: PublicKey
+    , _plaidEnvClientId    :: ClientId
+    , _plaidEnvSecret      :: Secret
+    , _plaidEnvEnvironment :: Environment
+    } deriving (Eq, Show)
+
+makeLenses ''PlaidEnv
+$(deriveFromJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 8 }) ''PlaidEnv)
+
+data PlaidBody a
+  = PlaidBody
+      { _plaidBodyEnv               :: PlaidEnv
+      , _plaidBodyPublicToken       :: Maybe PublicToken
+      , _plaidBodyAccessToken       :: Maybe AccessToken
+      , _plaidBodyOptions           :: Maybe PlaidOptions
+      , _plaidBodyPaginationOptions :: Maybe PlaidPaginationOptions
+      , _plaidBodyInstitutionId     :: Maybe InstitutionId
+      , _plaidBodyInitialProducts   :: Maybe [PlaidProduct]
+      , _plaidBodyStartDate         :: Maybe Day
+      , _plaidBodyEndDate           :: Maybe Day
+      } deriving (Eq, Show)
+
+makeLenses ''PlaidBody
+
+instance ToJSON (PlaidBody GetBalance) where
+  toJSON b =
+    if isNothing (b ^. plaidBodyOptions)
+      then
+        object [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)
+               , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)
+               , "access_token" .= toJSON (b ^. plaidBodyAccessToken)
+               ]
+      else
+        object [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)
+               , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)
+               , "access_token" .= (b ^. plaidBodyAccessToken)
+               , "options"      .= (b ^. plaidBodyOptions)
+               ]
+
+instance ToJSON (PlaidBody PublicTokenCreate) where
+  toJSON b =
+    if isNothing (b ^. plaidBodyOptions)
+      then
+        object [ "institution_id"   .= (b ^. plaidBodyInstitutionId)
+               , "public_key"       .= (b ^. plaidBodyEnv . plaidEnvPublicKey)
+               , "initial_products" .= (b ^. plaidBodyInitialProducts)
+               ]
+      else
+        object [ "institution_id"   .= (b ^. plaidBodyInstitutionId)
+               , "public_key"       .= (b ^. plaidBodyEnv . plaidEnvPublicKey)
+               , "initial_products" .= (b ^. plaidBodyInitialProducts)
+               , "options"          .= toJSON (b ^. plaidBodyOptions)
+               ]
+
+instance ToJSON (PlaidBody AuthGet) where
+  toJSON b =
+    object [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)
+           , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)
+           , "access_token" .= (b ^. plaidBodyAccessToken)
+           ]
+
+instance ToJSON (PlaidBody PlaidTokenExchange) where
+  toJSON b =
+    object [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)
+           , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)
+           , "public_token" .= (b ^. plaidBodyPublicToken)
+           ]
+
+instance ToJSON (PlaidBody PlaidTransactionsGet) where
+  toJSON b =
+    object $ [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)
+             , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)
+             , "access_token" .= (b ^. plaidBodyAccessToken)
+             , "start_date"   .= (b ^. plaidBodyStartDate)
+             , "end_date"     .= (b ^. plaidBodyEndDate)
+             ] <> perhapsSendObject
+    where
+      perhapsSendObject =
+        ["options" .= toJSON (b ^. plaidBodyPaginationOptions) | isJust (b ^. plaidBodyPaginationOptions)]
+
+instance ToJSON (PlaidBody PlaidIdentityGet) where
+  toJSON b =
+    object [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)
+           , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)
+           , "access_token" .= (b ^. plaidBodyAccessToken)
+           ]
+
+instance ToJSON (PlaidBody PlaidIncomeGet) where
+  toJSON b =
+    object [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)
+           , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)
+           , "access_token" .= (b ^. plaidBodyAccessToken)
+           ]
+
+-- | Main type used as the carrier for 'PlaidHttp' instance.
+-- We use mtl style constraints in library functions and at
+-- the end call 'runPlaid' to run all operations inside of IO.
+-- You never need to construct value of this type it is just used
+-- as a monad stack to carry Plaid operations.
+newtype Plaid a =
+  Plaid
+    { unPlaid :: ReaderT PlaidEnv IO a
+    } deriving newtype ( Functor
+                       , Applicative
+                       , Monad
+                       , MonadReader PlaidEnv
+                       , MonadIO
+                       , MonadThrow
+                       )
+
+-- | Carrier type used for testing
+newtype PlaidTest a =
+  PlaidTest
+    { unPlaidTest :: Identity a
+    } deriving newtype ( Functor
+                       , Applicative
+                       , Monad
+                       )
+
+runPlaid :: PlaidEnv -> Plaid a -> IO a
+runPlaid env = flip runReaderT env . unPlaid
+
+runTestPlaid :: PlaidTest a -> a
+runTestPlaid = runIdentity . unPlaidTest
+
+data CurrencyCode
+  = USD
+  | EUR
+  deriving (Eq, Show)
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 12 }) ''CurrencyCode)
+
+data Balance =
+  Balance
+    { balanceAvailable              :: Double
+    , balanceCurrent                :: Double
+    , balanceLimit                  :: Maybe Double
+    , balanceIsoCurrencyCode        :: CurrencyCode
+    , balanceUnofficialCurrencyCode :: Maybe CurrencyCode
+    } deriving (Eq, Show)
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 7 }) ''Balance)
+
+data Account =
+  Account
+    { accountAccountId    :: Text
+    , accountBalances     :: Balance
+    , accountMask         :: Text
+    , accountName         :: Text
+    , accountOfficialName :: Text
+    , accountSubtype      :: Text
+    , accountType         :: Text
+    } deriving (Eq, Show)
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 7 }) ''Account)
+
+data Ach =
+  Ach
+    { achAccount     :: AccountNumber
+    , achAccountId   :: AccountId
+    , achRouting     :: Text
+    , achWireRouting :: Text
+    } deriving (Eq, Show)
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 3 }) ''Ach)
+
+data Error =
+  Error
+    { errorType      :: Text
+    , errorCode      :: Text
+    , errorMessage   :: Text
+    , displayMessage :: Maybe Text
+    } deriving (Eq, Show)
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 5 }) ''Error)
+
+data Item =
+  Item
+    { itemAvailableProducts :: [Text]
+    , itemBilledProducts    :: [Text]
+    , itemError             :: Maybe Error
+    , itemInstitutionId     :: InstitutionId
+    , itemItemId            :: Text
+    , itemWebhook           :: Maybe Text
+    } deriving (Eq, Show)
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 4 }) ''Item)
+
+data Eft =
+  Eft
+    { eftAccount     :: AccountNumber
+    , eftAccountId   :: AccountId
+    , eftInstitution :: Institution
+    , eftBranch      :: Text
+    } deriving (Eq, Show)
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 3 }) ''Eft)
+
+data International =
+  International
+    { internationalAccountId :: AccountId
+    , internationalBic       :: Bic
+    , internationalIban      :: Iban
+    } deriving (Eq, Show)
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 13 }) ''International)
+
+data Bacs =
+  Bacs
+    { bacsAccount   :: AccountNumber
+    , bacsAccountId :: AccountId
+    , bacsSortCode  :: SortCode
+    } deriving (Eq, Show)
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 4 }) ''Bacs)
+
+data Numbers =
+  Numbers
+    { numbersAch           :: [Ach]
+    , numbersEft           :: [Eft]
+    , numbersInternational :: [International]
+    , numbersBacs          :: [Bacs]
+    } deriving (Eq, Show)
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 7 }) ''Numbers)
+
+data TransactionType
+  = Digital
+  | Place
+  | Special
+  | Unresolved
+  deriving (Eq, Show)
+
+instance ToJSON TransactionType where
+  toJSON Digital    = String "digital"
+  toJSON Place      = String "place"
+  toJSON Special    = String "special"
+  toJSON Unresolved = String "unresolved"
+
+instance FromJSON TransactionType where
+  parseJSON "digital"    = pure Digital
+  parseJSON "place"      = pure Place
+  parseJSON "special"    = pure Special
+  parseJSON "unresolved" = pure Unresolved
+  parseJSON _            = mempty
+
+data TransactionLocation =
+  TransactionLocation
+    { transactionLocationAddress       :: Maybe Text
+    , transactionLocationCity          :: Maybe Text
+    , transactionLocationRegion        :: Maybe Text
+    , transactionLocationPostalCode    :: Maybe Text
+    , transactionLocationPostalCountry :: Maybe Text
+    , transactionLocationPostalLat     :: Maybe Double
+    , transactionLocationPostalLon     :: Maybe Double
+    } deriving (Eq, Show)
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 19 }) ''TransactionLocation)
+
+data PaymentMeta =
+  PaymentMeta
+    { paymentMetaReferenceNumber :: Maybe Text
+    , paymentMetaPpdId           :: Maybe Text
+    , paymentMetaPayee           :: Maybe Text
+    } deriving (Eq, Show)
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 11 }) ''PaymentMeta)
+
+data Transaction =
+  Transaction
+    { transactionAccountId              :: AccountId
+    , transactionAmount                 :: Double
+    , transactionIsoCurrencyCode        :: CurrencyCode
+    , transactionUnofficialCurrencyCode :: Maybe CurrencyCode
+    , transactionCategory               :: Maybe [Text]
+    , transactionCategoryId             :: Maybe Text
+    , transactionTransactionType        :: Maybe TransactionType
+    , transactionName                   :: Text
+    , transactionDate                   :: Day
+    , transactionLocation               :: TransactionLocation
+    , transactionPending                :: Bool
+    , transactionPaymentMeta            :: PaymentMeta
+    , transactionPendingTransactionId   :: Maybe Text
+    , transactionAccountOwner           :: Maybe Text
+    } deriving (Eq, Show)
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 11 }) ''Transaction)
+
+data PlaidTransactionsGetResponse =
+  PlaidTransactionsGetResponse
+    { _plaidTransactionsGetResponseAccounts     :: [Account]
+    , _plaidTransactionsGetResponseTransactions :: [Transaction]
+    , _plaidTransactionsItem                    :: Item
+    , _plaidTransactionsTotalTransactions       :: Int
+    , _plaidTransactionsRequestId               :: Text
+    } deriving (Eq, Show)
+
+makeLenses ''PlaidTransactionsGetResponse
+
+$(deriveToJSON (defaultOptions { fieldLabelModifier = camel . drop 29 }) ''PlaidTransactionsGetResponse)
+
+instance FromJSON PlaidTransactionsGetResponse where
+  parseJSON = withObject "PlaidTransactionsGetResponse" $ \o ->
+    PlaidTransactionsGetResponse
+      <$> o .: "accounts"
+      <*> o .: "transactions"
+      <*> o .: "item"
+      <*> o .: "total_transactions"
+      <*> o .: "request_id"
+
+data PlaidAuthGetResponse =
+  PlaidAuthGetResponse
+    { _plaidAuthGetResponseAccounts  :: [Account]
+    , _plaidAuthGetResponseNumbers   :: Numbers
+    , _plaidAuthGetResponseItem      :: Item
+    , _plaidAuthGetResponseRequestId :: RequestId
+    } deriving (Eq, Show)
+
+makeLenses ''PlaidAuthGetResponse
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 21 }) ''PlaidAuthGetResponse)
+
+data PlaidPublicTokenResponse =
+  PlaidPublicTokenResponse
+    { _plaidPublicTokenResponsePublicToken :: PublicToken
+    , _plaidPublicTokenResponseRequestId   :: Text
+    } deriving (Eq, Show)
+
+makeLenses ''PlaidPublicTokenResponse
+
+instance ToJSON PlaidPublicTokenResponse where
+  toJSON PlaidPublicTokenResponse {..} =
+    object [ "public_token" .= _plaidPublicTokenResponsePublicToken
+           , "request_id"   .= _plaidPublicTokenResponseRequestId
+           ]
+
+instance FromJSON PlaidPublicTokenResponse where
+  parseJSON = withObject "PlaidPublicTokenResponse" $ \o ->
+    PlaidPublicTokenResponse
+      <$> o .: "public_token"
+      <*> o .: "request_id"
+
+data PlaidAccessTokenResponse =
+  PlaidAccessTokenResponse
+    { _plaidAccessTokenResponseAccessToken :: AccessToken
+    , _plaidAccessTokenResponseRequestId   :: Text
+    , _plaidAccessTokenResponseItemId      :: Text
+    } deriving (Eq, Show)
+
+makeLenses ''PlaidAccessTokenResponse
+
+instance ToJSON PlaidAccessTokenResponse where
+  toJSON PlaidAccessTokenResponse {..} =
+    object [ "access_token" .= _plaidAccessTokenResponseAccessToken
+           , "request_id"   .= _plaidAccessTokenResponseRequestId
+           , "item_id"      .= _plaidAccessTokenResponseItemId
+           ]
+
+instance FromJSON PlaidAccessTokenResponse where
+  parseJSON = withObject "PlaidAccessTokenResponse" $ \o ->
+    PlaidAccessTokenResponse
+      <$> o .: "access_token"
+      <*> o .: "request_id"
+      <*> o .: "item_id"
+
+data PhoneNumber =
+  PhoneNumber
+    { _phoneNumberData    :: Text
+    , _phoneNumberPrimary :: Bool
+    , _phoneNumberType    :: Text
+    } deriving (Eq, Show)
+
+makeLenses ''PhoneNumber
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 12 }) ''PhoneNumber)
+
+data Email =
+  Email
+    { _emailData    :: Text
+    , _emailPrimary :: Bool
+    , _emailType    :: Text
+    } deriving (Eq, Show)
+
+makeLenses ''Email
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 6 }) ''Email)
+
+data Address =
+  Address
+    { _addressCity       :: Text
+    , _addressRegion     :: Text
+    , _addressStreet     :: Text
+    , _addressPostalCode :: Text
+    , _addressCountry    :: Maybe Text
+    } deriving (Eq, Show)
+
+makeLenses ''Address
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 8 }) ''Address)
+
+data Addresses =
+  Addresses
+    { _addressesData    :: Address
+    , _addressesPrimary :: Bool
+    } deriving (Eq, Show)
+
+makeLenses ''Addresses
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 10 }) ''Addresses)
+
+data Owners =
+  Owners
+    { _ownersAddresses    :: [Addresses]
+    , _ownersEmails       :: [Email]
+    , _ownersNames        :: [Text]
+    , _ownersPhoneNumbers :: [PhoneNumber]
+    } deriving (Eq, Show)
+
+makeLenses ''Owners
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 7 }) ''Owners)
+
+data Accounts =
+  Accounts
+    { _accountsAccountId    :: Text
+    , _accountsBalances     :: Balance
+    , _accountsMask         :: Text
+    , _accountsName         :: Text
+    , _accountsOfficialName :: Maybe Text
+    , _accountsOwners       :: [Owners]
+    , _accountsSubtype      :: Text
+    , _accountsType         :: Text
+    } deriving (Eq, Show)
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 9 }) ''Accounts)
+
+data PlaidIdentityGetResponse =
+  PlaidIdentityGetResponse
+    { _plaidIdentityGetResponseAccounts :: [Accounts]
+    , _plaidIdentityGetItem             :: Item
+    , _plaidIdentityGetRequestId        :: Text
+    } deriving (Eq, Show)
+
+makeLenses ''PlaidIdentityGetResponse
+
+$(deriveToJSON (defaultOptions { fieldLabelModifier = camel . drop 25 }) ''PlaidIdentityGetResponse)
+
+instance FromJSON PlaidIdentityGetResponse where
+  parseJSON = withObject "PlaidIdentityGetResponse" $ \o ->
+    PlaidIdentityGetResponse
+      <$> o .: "accounts"
+      <*> o .: "item"
+      <*> o .: "request_id"
+
+data IncomeStream =
+  IncomeStream
+    { _incomeStreamMonthlyIncome :: Fixed E2
+    , _incomeStreamConfidence    :: Fixed E2
+    , _incomeStreamDays          :: Fixed E2
+    , _incomeStreamName          :: Text
+    } deriving (Eq, Show)
+
+makeLenses ''IncomeStream
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = camel . drop 13 }) ''IncomeStream)
+
+data Income =
+  Income
+    { _incomeLastYearIncome                      :: Fixed E2
+    , _incomeLastYearIncomeBeforeTax             :: Fixed E2
+    , _incomeProjectedYearlyIncome               :: Fixed E2
+    , _incomeProjectedYearlyIncomeBeforeTax      :: Fixed E2
+    , _incomeIncomeStreams                       :: [IncomeStream]
+    , _incomeMaxNumberOfOverlappingIncomeStreams :: Fixed E2
+    , _incomeNumberOfIncomeStreams               :: Fixed E2
+    } deriving (Eq, Show)
+
+makeLenses ''Income
+
+$(deriveJSON (defaultOptions { fieldLabelModifier = camel . drop 7 }) ''Income)
+
+baseUrl :: Environment -> Url Environment
+baseUrl e = Url $ T.pack $ "https://" <> show e <> ".plaid.com"
+
+envUrl :: Environment -> Text
+envUrl = unUrl . baseUrl
+
+validInstitutionIds :: [InstitutionId]
+validInstitutionIds =
+  InstitutionId <$>
+    [ "ins_1"
+    , "ins_2"
+    , "ins_3"
+    , "ins_4"
+    , "ins_5"
+    , "ins_6"
+    , "ins_7"
+    , "ins_9"
+    , "ins_10"
+    , "ins_11"
+    , "ins_13"
+    , "ins_14"
+    , "ins_15"
+    , "ins_16"
+    , "ins_19"
+    , "ins_20"
+    , "ins_21"
+    , "ins_23"
+    , "ins_24"
+    , "ins_27"
+    , "ins_29"
+    ]
+
+-- | Map used for testing different plaid endpoints
+requestMap :: M.Map Text ByteString
+requestMap =
+  M.fromList
+    [ ("/auth/get", responseAuthGet)
+    , ("/transactions/get", responseTransactionsGet)
+    , ("/public_token/create", responsePublicTokenCreate)
+    , ("/item/public_token/exchange", responsePublicTokenExchange)
+    , ("/identity/get", identityJson)
+    ]
+
+-- | Instances
+instance PlaidHttp Plaid where
+  executeGet :: Text -> Plaid ByteString
+  executeGet url = do
+    request <- parseUrlThrow (T.unpack url)
+    m <- liftIO $ newManager tlsManagerSettings
+    response <- httpLbs request m
+    return $ responseBody response
+
+  executePost :: ToJSON a => Text -> a -> Plaid ByteString
+  executePost url postBody = do
+    initialRequest <- parseUrlThrow (T.unpack url)
+    m <- liftIO (newManager tlsManagerSettings)
+    let request =
+          initialRequest
+          { method = "POST"
+          , requestBody = RequestBodyLBS (encode postBody)
+          , requestHeaders = [ ("Content-Type", "application/json") ]
+          }
+    response <- httpLbs request m
+    return $ responseBody response
+
+instance PlaidHttp PlaidTest where
+  executeGet :: Text -> PlaidTest ByteString
+  executeGet url =
+    case M.lookup url requestMap of
+      Nothing -> return ""
+      Just bs -> return bs
+
+  executePost :: ToJSON a => Text -> a -> PlaidTest ByteString
+  executePost url _postBody =
+    case M.lookup url requestMap of
+      Nothing -> return ""
+      Just bs -> return bs
+
+instance Semigroup PlaidOptions where
+  (<>) _ b = b
+
+instance Monoid PlaidOptions where
+  mempty =
+    PlaidOptions
+      { _plaidOptionsWebHook          = ""
+      , _plaidOptionsOverrideUsername = ""
+      , _plaidOptionsOverridePassword = ""
+      }
+  mappend _ b = b
+
+instance Semigroup (PlaidBody a) where
+  (<>) _ b = b
+
+instance Monoid (PlaidBody a) where
+  mempty =
+    let env =
+          PlaidEnv
+            { _plaidEnvPublicKey   = ""
+            , _plaidEnvClientId    = ClientId ""
+            , _plaidEnvSecret      = Secret ""
+            , _plaidEnvEnvironment = Sandbox
+            }
+    in
+      PlaidBody
+        { _plaidBodyEnv               = env
+        , _plaidBodyPublicToken       = Nothing
+        , _plaidBodyAccessToken       = Nothing
+        , _plaidBodyOptions           = Nothing
+        , _plaidBodyPaginationOptions = Nothing
+        , _plaidBodyInstitutionId     = Nothing
+        , _plaidBodyInitialProducts   = Nothing
+        , _plaidBodyStartDate         = Nothing
+        , _plaidBodyEndDate           = Nothing
+        }
+  mappend _ b = b
+
diff --git a/Data/Common.hs b/Data/Common.hs
new file mode 100644
--- /dev/null
+++ b/Data/Common.hs
@@ -0,0 +1,25 @@
+{-# OPTIONS_HADDOCK hide, prune, ignore-exports #-}
+module Data.Common
+  ( module Data.Api.Types
+  , module Data.Api.Helper
+  , module Data.Aeson
+  , module Lens.Micro
+  , MonadReader (..)
+  , MonadThrow
+  , MonadIO (..)
+  , ReaderT (..)
+  , ByteString
+  , fromStrict
+  , toStrict
+  , Text
+  ) where
+
+import           Control.Monad.Reader (MonadReader (..), ReaderT (..))
+import           Data.Api.Helper
+import           Data.Api.Types
+import           Data.Aeson hiding (Error)
+import           Data.ByteString.Lazy (ByteString, fromStrict, toStrict)
+import           Data.Text            (Text)
+import           Control.Exception.Safe
+import           Control.Monad.IO.Class
+import           Lens.Micro
diff --git a/Data/Plaid.hs b/Data/Plaid.hs
new file mode 100644
--- /dev/null
+++ b/Data/Plaid.hs
@@ -0,0 +1,21 @@
+-- |
+--
+-- Module      :  Data.Plaid
+-- Copyright   :  2019 Sasha Bogicevic
+-- License     :  BSD 3 clause
+--
+-- Maintainer  :  Sasha Bogicevic <sasa.bogicevic@pm.me>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- === Welcome to Plaid
+-- As you could guess this library aims to provide easy
+-- integration with Plaid.com
+
+module Data.Plaid
+  ( module Data.Api.Plaid
+  , module Data.Api.Types
+  ) where
+
+import           Data.Api.Plaid
+import           Data.Api.Types hiding (Plaid, PlaidTest, envUrl, requestMap, runTestPlaid)
diff --git a/Data/Proof/AuthGetBody.hs b/Data/Proof/AuthGetBody.hs
new file mode 100644
--- /dev/null
+++ b/Data/Proof/AuthGetBody.hs
@@ -0,0 +1,19 @@
+module Data.Proof.AuthGetBody
+  ( HasAuthGetBody
+  , proveAuthGetBody
+  ) where
+
+import           Data.Api.Types
+import           Data.Proof.Named
+import           Data.Text (Text)
+
+data HasAuthGetBody plaidEnv accessToken = Proof
+
+proveAuthGetBody
+  :: Named plaidEnv PlaidEnv
+  -> Named accessToken AccessToken
+  -> Either Text (HasAuthGetBody plaidEnv accessToken)
+proveAuthGetBody _ atoken =
+  case unWrapNamed atoken of
+    (AccessToken "") -> Left "AccessToken cannot be empty"
+    (AccessToken _ ) -> Right Proof
diff --git a/Data/Proof/BalanceGetBody.hs b/Data/Proof/BalanceGetBody.hs
new file mode 100644
--- /dev/null
+++ b/Data/Proof/BalanceGetBody.hs
@@ -0,0 +1,20 @@
+module Data.Proof.BalanceGetBody
+  ( HasGetBalanceBody
+  , proveGetBalanceBody
+  ) where
+
+import           Data.Api.Types
+import           Data.Proof.Named
+import           Data.Text (Text)
+
+data HasGetBalanceBody plaidEnv accessToken = Proof
+
+-- | Check for creation of get balance request body
+proveGetBalanceBody
+  :: Named plaidEnv PlaidEnv
+  -> Named accessToken AccessToken
+  -> Either Text (HasGetBalanceBody plaidEnv accessToken)
+proveGetBalanceBody _ atoken =
+  case unWrapNamed atoken of
+    (AccessToken "") -> Left "AccessToken cannot be empty"
+    (AccessToken _ ) -> Right Proof
diff --git a/Data/Proof/IdentityGetBody.hs b/Data/Proof/IdentityGetBody.hs
new file mode 100644
--- /dev/null
+++ b/Data/Proof/IdentityGetBody.hs
@@ -0,0 +1,20 @@
+module Data.Proof.IdentityGetBody
+  ( HasIdentityGetBody
+  , proveIdentityGetBody
+  ) where
+
+import           Data.Api.Types
+import           Data.Proof.Named
+import           Data.Text (Text)
+
+data HasIdentityGetBody plaidEnv accessToken = Proof
+
+-- | Check for creation of get identity request body
+proveIdentityGetBody
+  :: Named plaidEnv PlaidEnv
+  -> Named accessToken AccessToken
+  -> Either Text (HasIdentityGetBody plaidEnv accessToken)
+proveIdentityGetBody _ atoken =
+  case unWrapNamed atoken of
+    (AccessToken "") -> Left "AccessToken cannot be empty"
+    (AccessToken _ ) -> Right Proof
diff --git a/Data/Proof/IncomeGetBody.hs b/Data/Proof/IncomeGetBody.hs
new file mode 100644
--- /dev/null
+++ b/Data/Proof/IncomeGetBody.hs
@@ -0,0 +1,20 @@
+module Data.Proof.IncomeGetBody
+  ( HasIncomeGetBody
+  , proveIncomeGetBody
+  ) where
+
+import           Data.Api.Types
+import           Data.Proof.Named
+import           Data.Text (Text)
+
+data HasIncomeGetBody plaidEnv accessToken = Proof
+
+-- | Check for creation of get income request body
+proveIncomeGetBody
+  :: Named plaidEnv PlaidEnv
+  -> Named accessToken AccessToken
+  -> Either Text (HasIncomeGetBody plaidEnv accessToken)
+proveIncomeGetBody _ atoken =
+  case unWrapNamed atoken of
+    (AccessToken "") -> Left "AccessToken cannot be empty"
+    (AccessToken _ ) -> Right Proof
diff --git a/Data/Proof/Named.hs b/Data/Proof/Named.hs
new file mode 100644
--- /dev/null
+++ b/Data/Proof/Named.hs
@@ -0,0 +1,15 @@
+{-# LANGUAGE Rank2Types #-}
+
+module Data.Proof.Named
+  ( Named
+  , name
+  , unWrapNamed
+  ) where
+
+newtype Named n a = Named { forgetName :: a } deriving Show
+
+name :: a -> ( forall name. Named name a -> r ) -> r
+name x f = f ( Named x )
+
+unWrapNamed :: Named n a -> a
+unWrapNamed = forgetName
diff --git a/Data/Proof/Proof.hs b/Data/Proof/Proof.hs
new file mode 100644
--- /dev/null
+++ b/Data/Proof/Proof.hs
@@ -0,0 +1,25 @@
+module Data.Proof.Proof
+  ( HasAuthGetBody
+  , HasGetBalanceBody
+  , HasCreatePublicTokenBody
+  , HasTransactionsGetBody
+  , HasIdentityGetBody
+  , HasIncomeGetBody
+  , name
+  , unWrapNamed
+  , proveAuthGetBody
+  , proveTransactionsGetBody
+  , proveIdentityGetBody
+  , proveGetBalanceBody
+  , proveCreatePublicTokenBody
+  , proveIncomeGetBody
+  ) where
+
+import           Data.Proof.AuthGetBody         (HasAuthGetBody, proveAuthGetBody)
+import           Data.Proof.BalanceGetBody      (HasGetBalanceBody, proveGetBalanceBody)
+import           Data.Proof.IdentityGetBody     (HasIdentityGetBody, proveIdentityGetBody)
+import           Data.Proof.IncomeGetBody       (HasIncomeGetBody, proveIncomeGetBody)
+import           Data.Proof.Named               (name, unWrapNamed)
+import           Data.Proof.PublicTokenCreate   (HasCreatePublicTokenBody,
+                                                 proveCreatePublicTokenBody)
+import           Data.Proof.TransactionsGetBody (HasTransactionsGetBody, proveTransactionsGetBody)
diff --git a/Data/Proof/PublicTokenCreate.hs b/Data/Proof/PublicTokenCreate.hs
new file mode 100644
--- /dev/null
+++ b/Data/Proof/PublicTokenCreate.hs
@@ -0,0 +1,25 @@
+module Data.Proof.PublicTokenCreate
+  ( HasCreatePublicTokenBody
+  , proveCreatePublicTokenBody
+  ) where
+
+import           Data.Api.Types
+import           Data.Proof.Named
+import           Data.Text        (Text)
+
+data HasCreatePublicTokenBody plaidEnv institutionId productList plaidOptions = Proof
+
+-- | Check if all elements for creating a public token are present
+proveCreatePublicTokenBody
+  :: Named plaidEnv PlaidEnv
+  -> Named institutionId InstitutionId
+  -> Named productList [PlaidProduct]
+  -> Named options (Maybe PlaidOptions)
+  -> Either Text (HasCreatePublicTokenBody plaidEnv institutionId productList options)
+proveCreatePublicTokenBody _ institutionId productList _
+  | null (unWrapNamed productList) =
+      Left "Initial products list cannot be empty"
+  | unWrapNamed institutionId `elem` validInstitutionIds
+      = Right Proof
+  | otherwise = Left (unInstitutionId (unWrapNamed institutionId)
+                  <> " is not a valid institution id. Check the plaid documentation.")
diff --git a/Data/Proof/TransactionsGetBody.hs b/Data/Proof/TransactionsGetBody.hs
new file mode 100644
--- /dev/null
+++ b/Data/Proof/TransactionsGetBody.hs
@@ -0,0 +1,24 @@
+module Data.Proof.TransactionsGetBody
+  ( HasTransactionsGetBody
+  , proveTransactionsGetBody
+  ) where
+
+import           Data.Api.Types
+import           Data.Proof.Named
+import           Data.Text        (Text)
+import           Data.Time        (Day)
+
+data HasTransactionsGetBody plaidEnv accessToken startDate endDate options paginationOptions = Proof
+
+proveTransactionsGetBody
+  :: Named plaidEnv PlaidEnv
+  -> Named accessToken AccessToken
+  -> Named startDate Day
+  -> Named endDate Day
+  -> Named options (Maybe PlaidOptions)
+  -> Named paginationOptions (Maybe PlaidPaginationOptions)
+  -> Either Text (HasTransactionsGetBody plaidEnv accessToken startDate endDate options paginationOptions)
+proveTransactionsGetBody _ atoken _ _ _ _ =
+  case unWrapNamed atoken of
+    (AccessToken "") -> Left "AccessToken cannot be empty"
+    (AccessToken _ ) -> Right Proof
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Sasa Bogicevic (c) 2019
+
+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 Sasa Bogicevic 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,29 @@
+![](https://plaid.com/assets/img/navbar/logo.svg) 
+
+
+[![Build Status](https://travis-ci.org/v0d1ch/plaid.png)](https://travis-ci.org/v0d1ch/plaid)
+
+
+This is a Haskell library for interacting with [plaid.com](https://plaid.com) api.
+
+It utilizes some of the nice ideas presented in the [GDP](https://kataskeue.com/gdp.pdf) paper and in general tries to stay out of your way and let you issue plaid.com api calls easily.
+
+You should check out the [/examples/Main.hs](https://github.com/v0d1ch/plaid/blob/master/examples/Main.hs) to see some of the examples of using
+the lib.
+
+#### Contributing
+
+
+I encourage you to play with the library, open up issues, get yourself involved.
+
+Plan is to build nice documentation with helpful examples.
+
+I don't have specific code style guidelines, just follow the code style that you
+already see. 
+
+There is a stylish-haskell.yaml file in the root and it would be nice if you can configure your editor to use it.
+
+Please squash your commits into one when issuing a PR so we keep a nice git timeline.
+
+
+
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/examples/Main.hs b/examples/Main.hs
new file mode 100644
--- /dev/null
+++ b/examples/Main.hs
@@ -0,0 +1,231 @@
+module Main where
+
+{-
+ Module containing useful real world examples of using plaid library.
+-}
+
+import           Control.Exception.Safe (MonadThrow, throwM)
+import           Control.Monad.Except   (liftEither, runExceptT)
+import           Control.Monad.Reader
+import           Data.ByteString.Lazy   (ByteString)
+import           Data.Plaid
+import           Data.Text              (Text)
+import           Data.Time
+import           Text.Pretty.Simple
+
+-- | Client id
+clientId :: ClientId
+clientId = ClientId "5d11dd0fdeb8a80013cc99e9"
+
+-- | Public key
+publicKey :: PublicKey
+publicKey = "f19293a734d8e9cc89faacb09719e5"
+
+-- | Sandbox secret key
+sandboxSecret :: Secret
+sandboxSecret = Secret "0d5e0ec25d432388f7fb34d3528394"
+
+-- | Public token
+publicToken :: PublicToken
+publicToken = PublicToken "public-sandbox-e5c3a0d9-822b-4d8c-bd13-4253485c99b2"
+
+-- | Access token
+accessToken :: AccessToken
+accessToken =  AccessToken "access-sandbox-9e96926d-4da6-4620-a13b-c72980976436"
+
+-- | Example of item id
+itemId :: Text
+itemId = "Qlr98nQgkdTQyM8agmjGt3a4RV6lKxup88BLV"
+
+-- | Main function from which we call different api endpoints for demonstration
+main :: IO ()
+main = do
+  let env = PlaidEnv publicKey clientId sandboxSecret Sandbox
+  exchangeTokenResponse <- runPlaid env exampleExchangeToken
+  pPrint exchangeTokenResponse
+  authResponse <- runPlaid env exampleRetrieveAuthRequest
+  pPrint authResponse
+  transactionsResponse <- runPlaid env exampleRetrieveTransactionsRequest
+  pPrint transactionsResponse
+  identityResponse <- runPlaid env exampleIdentityRequest
+  pPrint identityResponse
+  incomeResponse <- runPlaid env exampleIncomeRequest
+  pPrint incomeResponse
+
+-- | POST /item/public_token/exchange
+--  https://plaid.com/docs/#exchange-token-flow
+exampleExchangeToken
+  :: ( MonadReader PlaidEnv m
+     , MonadIO m
+     , MonadThrow m
+     , PlaidHttp m
+     )
+  => m (Either PlaidError PlaidAccessTokenResponse)
+exampleExchangeToken = do
+  -- get the plaid environment
+  env <- ask
+  runExceptT $ do
+    -- create the public token request body
+    -- this action can fail if we provide wrong parameters
+    publicTokenBody <- liftEither $
+        mkCreatePublicTokenEnv env
+          -- Pay attention to provide valid institution id! There is a list of valid ones
+          -- in the docs https:://plaid.com/docs/#investments-top-institutions-we-cover
+          (InstitutionId "ins_3")
+          -- Provide the list of plaid products or empty list
+          ["transactions"]
+          -- This is optional. If you want you can setup a web-hook where plaid will send its responses.
+          defaultPlaidOptions
+    -- fire the create public token action
+    PlaidPublicTokenResponse {..} <- lift $ plaidCreatePublicToken publicTokenBody
+    -- use the response to create an exchange public token request body
+    -- it looks something like this:
+    -- @
+    -- PlaidBody {plaidBodyEnv = PlaidEnv {plaidEnvPublicKey = "f19293a734d8e9cc89faacb09719e5", plaidEnvClientId = ClientId {unClientId = "5d11dd0fdeb8a80013cc99e9"}, plaidEnvSecret = Secret {unSecret = "0d5e0ec25d432388f7fb34d3528394"}, plaidEnvEnvironment = sandbox}, plaidBodyPublicToken = Just (PublicToken {unPublicToken = "public-sandbox-6a6d0da0-4d53-4702-a1eb-9b430625122b"}), plaidBodyAccessToken = Nothing, plaidBodyOptions = Nothing, plaidBodyInstitutionId = Nothing, plaidBodyInitialProducts = Nothing}
+    -- @
+
+    exchangeBody <- liftEither (mkExchangePublicTokenEnv env _plaidPublicTokenResponsePublicToken)
+    -- fire exchange token action
+    lift $ plaidExchangeToken exchangeBody
+    -- if the response is successfull it looks something like this:
+    -- @
+    -- Right (PlaidAccessTokenResponse {plaidAccessTokenResponseAccessToken = "access-sandbox-0ca6b500-c682-437e-bdc9-051665b71941", plaidAccessTokenResponseRequestId = "2vBC1rv95r7Se60", plaidAccessTokenResponseItemId = "mdwNyLvv9zF655lGQ1bxikExVGb4doULb5VPp"})
+    -- @
+    
+getAuthBody
+  :: ( MonadReader PlaidEnv m
+     , MonadIO m
+     , MonadThrow m
+     , PlaidHttp m
+     )
+  => m (Either PlaidError (PlaidBody AuthGet))
+getAuthBody = do
+  env <- ask
+  runExceptT $ do
+    publicTokenBody <- liftEither $ createPublicTokenBody env
+    PlaidAccessTokenResponse {..} <- lift $ exchangeToken publicTokenBody
+    liftEither (mkGetAuthEnv env _plaidAccessTokenResponseAccessToken)
+
+-- | POST /auth/get
+-- https://plaid.com/docs/#retrieve-auth-request
+exampleRetrieveAuthRequest
+  :: ( MonadReader PlaidEnv m
+     , MonadIO m
+     , MonadThrow m
+     , PlaidHttp m
+     )
+  => m (Either PlaidError ByteString)
+exampleRetrieveAuthRequest =
+  -- get the plaid environment
+  runExceptT $ do
+    -- create the public token request body
+    -- this action can fail if we provide wrong parameters
+    eauthBody <- lift getAuthBody
+    authBody <- liftEither eauthBody
+    lift $ plaidGetAuth authBody
+
+-- | POST /transactions/get
+-- https://plaid.com/docs/#retrieve-transactions-request
+exampleRetrieveTransactionsRequest
+  :: ( MonadReader PlaidEnv m
+     , MonadIO m
+     , MonadThrow m
+     , PlaidHttp m
+     )
+  => m (Either PlaidError ByteString)
+exampleRetrieveTransactionsRequest = do
+  -- get the plaid environment
+  env <- ask
+  runExceptT $ do
+    -- create the public token request body
+    -- this action can fail if we provide wrong parameters
+    publicTokenBody <- liftEither $ createPublicTokenBody env
+    PlaidAccessTokenResponse {..} <- lift $ exchangeToken publicTokenBody
+    authBody <- liftEither (mkGetAuthEnv env _plaidAccessTokenResponseAccessToken)
+    _ <- lift $ plaidGetAuth authBody
+    let startTime = fromGregorian 2018 1 1
+        endTime   = fromGregorian 2019 1 1
+    transactionBody <-
+      liftEither
+        (mkCreateTransactionsGetEnv
+           env
+           _plaidAccessTokenResponseAccessToken
+           startTime
+           endTime
+           Nothing
+           (Just defPaginationOptions)
+        )
+    lift $ plaidGetTransactions transactionBody
+-- | POST /income/get
+-- http://plaid.com/docs/#income
+exampleIncomeRequest
+  :: ( MonadReader PlaidEnv m
+     , MonadIO m
+     , MonadThrow m
+     , PlaidHttp m
+     )
+  => m (Either PlaidError ByteString)
+exampleIncomeRequest = do
+  env <- ask
+  runExceptT $ do
+    -- create the public token request body
+    -- this action can fail if we provide wrong parameters
+    publicTokenBody <- liftEither $ createPublicTokenBody env
+    PlaidAccessTokenResponse {..} <- lift $ exchangeToken publicTokenBody
+    incomeBody <- liftEither (mkCreateIncomeGetEnv env _plaidAccessTokenResponseAccessToken)
+    lift $ plaidGetIncome incomeBody
+
+-- | POST /identity/get
+-- http://plaid.com/docs/#identity
+exampleIdentityRequest
+  :: ( MonadReader PlaidEnv m
+     , MonadIO m
+     , MonadThrow m
+     , PlaidHttp m
+     )
+  => m (Either PlaidError ByteString)
+exampleIdentityRequest = do
+  env <- ask
+  runExceptT $ do
+    -- create the public token request body
+    -- this action can fail if we provide wrong parameters
+    publicTokenBody <- liftEither $ createPublicTokenBody env
+    PlaidAccessTokenResponse {..} <- lift $ exchangeToken publicTokenBody
+    identityBody <- liftEither (mkCreateIdentityGetEnv env _plaidAccessTokenResponseAccessToken)
+    lift $ plaidGetIdentity identityBody
+
+exchangeToken
+  :: ( MonadReader PlaidEnv m
+     , MonadIO m
+     , MonadThrow m
+     , PlaidHttp m
+     )
+  => PlaidBody PublicTokenCreate
+  -> m PlaidAccessTokenResponse
+exchangeToken publicTokenBody = do
+  env <- ask
+  -- fire the create public token action
+  PlaidPublicTokenResponse {..} <- plaidCreatePublicToken publicTokenBody
+  case mkExchangePublicTokenEnv env _plaidPublicTokenResponsePublicToken of
+    Left err           -> throwM err
+    Right exchangeBody -> plaidExchangeToken exchangeBody
+
+createPublicTokenBody :: PlaidEnv -> Either PlaidError (PlaidBody PublicTokenCreate)
+createPublicTokenBody env =
+  mkCreatePublicTokenEnv env
+    -- pay attention to provide valid institution id! There is a list of valid ones
+    -- in the docs https:://plaid.com/docs/#investments-top-institutions-we-cover
+    (InstitutionId "ins_3")
+    -- provide the list of plaid products or empty list
+    ["transactions"]
+    -- this is optional. If you want you can setup a web-hook where plaid will send its responses.
+    mempty
+
+defaultPlaidOptions :: Maybe PlaidOptions
+defaultPlaidOptions =
+  Just $
+     PlaidOptions
+       { _plaidOptionsWebHook = "http://bogicevicsasa.com"
+       , _plaidOptionsOverrideUsername = "user_good"
+       , _plaidOptionsOverridePassword = "pass_good"
+       }
diff --git a/plaid.cabal b/plaid.cabal
new file mode 100644
--- /dev/null
+++ b/plaid.cabal
@@ -0,0 +1,165 @@
+cabal-version: 1.12
+name:           plaid
+version:        0.1.0.0
+synopsis:       Plaid.com api integration library
+description: @Plaid@ is a library for interacting with https://plaid.com
+  .
+  Plaid.com connects the users to their bank accounts via the app and this library provides programmatic
+  access to all these features.
+  .
+  Please take a look at examples folder to easily get started.
+  .
+  Haskell examples in the plaid [documentation] (https://plaid.com/docs) come from this library.
+  .
+category:       Data, Web, Network, Plaid
+homepage:       https://github.com/v0d1ch/plaid#readme
+bug-reports:    https://github.com/v0d1ch/plaid/issues
+author:         Sasha Bogicevic
+maintainer:     Sasha Bogicevic <sasa.bogicevic@pm.me>
+copyright:      2019 Sasa Bogicevic
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    ChangeLog.md
+
+source-repository head
+  type: git
+  location: https://github.com/v0d1ch/plaid
+
+library
+  exposed-modules:
+      Data.Plaid
+      Data.Common
+      Data.Api.TestByteStrings
+  other-modules:
+      Data.Api.Helper
+      Data.Api.InternalPure
+      Data.Api.Link
+      Data.Api.Transactions
+      Data.Api.Identity
+      Data.Api.Income
+      Data.Api.Plaid
+      Data.Api.Accounts
+      Data.Api.Auth
+      Data.Api.Types
+      Data.Proof.TransactionsGetBody
+      Data.Proof.IdentityGetBody
+      Data.Proof.IncomeGetBody
+      Data.Proof.AuthGetBody
+      Data.Proof.BalanceGetBody
+      Data.Proof.Named
+      Data.Proof.Proof
+      Data.Proof.PublicTokenCreate
+
+  ghc-options: -Wall
+  build-depends:
+      aeson
+    , base >= 4.6 && < 4.13
+    , bytestring
+    , mtl
+    , text
+    , network
+    , http-conduit
+    , http-client-tls
+    , safe-exceptions
+    , containers
+    , casing
+    , raw-strings-qq
+    , time
+    , microlens
+    , microlens-th
+    , pretty-simple
+  default-language: Haskell2010
+  default-extensions: OverloadedStrings
+                      RecordWildCards
+                      FlexibleContexts
+                      ScopedTypeVariables
+                      DataKinds
+                      DeriveAnyClass
+                      DeriveGeneric
+                      DerivingStrategies
+                      FlexibleContexts
+                      FlexibleInstances
+                      GADTs
+                      GeneralizedNewtypeDeriving
+                      InstanceSigs
+                      MultiParamTypeClasses
+                      TemplateHaskell
+                      TypeFamilies
+                      UndecidableInstances
+                      StrictData
+
+
+test-suite spec
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Paths_plaid
+      Plaid.PlaidSpec
+  hs-source-dirs:
+      test
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      aeson
+    , base >= 4.6 && < 4.13
+    , containers
+    , bytestring
+    , text
+    , hspec
+    , hspec-wai
+    , hspec-wai-json
+    , http-types
+    , QuickCheck
+    , wai
+    , plaid
+    , errors
+    , time
+    , pretty-simple
+    , microlens
+    , microlens-th
+  default-language: Haskell2010
+  default-extensions: OverloadedStrings
+                      RecordWildCards
+                      FlexibleContexts
+                      TemplateHaskell
+                      TypeFamilies
+                      ScopedTypeVariables
+                      StrictData
+                      GADTs
+
+executable plaid
+  main-is: Main.hs
+  hs-source-dirs:
+      examples
+
+  default-language: Haskell2010
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      aeson
+    , base >= 4.6 && < 4.13
+    , bytestring
+    , mtl
+    , text
+    , transformers
+    , conduit
+    , conduit-extra
+    , network
+    , http-client
+    , http-client-tls
+    , either
+    , safe-exceptions
+    , plaid
+    , time
+    , pretty-simple
+    , microlens
+    , microlens-th
+  default-extensions: OverloadedStrings
+                      RecordWildCards
+                      FlexibleContexts
+                      ScopedTypeVariables
+                      TemplateHaskell
+                      TypeFamilies
+                      GADTs
+                      StrictData
diff --git a/test/Plaid/PlaidSpec.hs b/test/Plaid/PlaidSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Plaid/PlaidSpec.hs
@@ -0,0 +1,162 @@
+module Plaid.PlaidSpec (spec) where
+
+import qualified Control.Error            as ER
+import           Data.Api.TestByteStrings
+import           Data.ByteString.Char8    (unpack)
+import           Data.Common
+import           Data.Either              (isRight)
+import           Data.Plaid
+import qualified Data.Text                as T
+import           Data.Time                (fromGregorian)
+import           Debug.Trace
+import           Test.Hspec
+
+_runTestGetRequest :: PlaidHttp m => Text -> m ByteString
+_runTestGetRequest url = executeGet url
+
+runTestPostRequest :: (PlaidHttp m, ToJSON a) => Text -> a -> m ByteString
+runTestPostRequest url postBody = executePost url postBody
+
+checkResult :: FromJSON a => ByteString -> Either PlaidError a
+checkResult result =
+  case result of
+    "" -> Left (PlaidError $ T.pack $ "Failed to decode bytestring " ++ (unpack $ toStrict result))
+    _  ->
+      case eitherDecode' result of
+        Left e   -> Left $ PlaidError (T.pack e)
+        Right ok -> Right ok
+
+env :: PlaidEnv
+env = PlaidEnv "" (ClientId "") (Secret "") Sandbox
+
+publicTokenBody :: Either PlaidError (PlaidBody PublicTokenCreate)
+publicTokenBody =
+  mkCreatePublicTokenEnv env
+    (InstitutionId "ins_3")
+    ["transactions"]
+    Nothing
+    -- (Just $
+    --   PlaidOptions
+    --   "http://bogicevicsasa.com"
+    --   "user_good"
+    --   "pass_good"
+    -- )
+    
+spec :: Spec
+spec = do
+  describe "Create public token" $ do
+    it "/public_token/create" $ do
+      let result = runTestPlaid $ runTestPostRequest "/public_token/create" publicTokenBody
+          testResult = checkResult result :: Either PlaidError PlaidPublicTokenResponse
+      isRight testResult `shouldBe` True
+
+  describe "Exchange public token" $ do
+    it "/item/public_token/exchange" $ do
+      let result = runTestPlaid $ runTestPostRequest "/public_token/create" publicTokenBody
+      testResult <- ER.runExceptT $ do
+        PlaidPublicTokenResponse {..} <- ER.hoistEither (checkResult result :: Either PlaidError PlaidPublicTokenResponse)
+        exchangeBody <- ER.hoistEither (mkExchangePublicTokenEnv env _plaidPublicTokenResponsePublicToken)
+        let result' = runTestPlaid $ runTestPostRequest "/item/public_token/exchange" exchangeBody
+        ER.hoistEither (checkResult result' :: Either PlaidError PlaidAccessTokenResponse)
+      isRight testResult `shouldBe` True
+
+  describe "Auth get" $ do
+    it "/auth/get" $ do
+      let result = runTestPlaid $ runTestPostRequest "/public_token/create" publicTokenBody
+      testResult <- ER.runExceptT $ do
+        PlaidPublicTokenResponse {..} <- ER.hoistEither (checkResult result :: Either PlaidError PlaidPublicTokenResponse)
+        exchangeBody <- ER.hoistEither (mkExchangePublicTokenEnv env _plaidPublicTokenResponsePublicToken)
+        let result' = runTestPlaid $ runTestPostRequest "/item/public_token/exchange" exchangeBody
+        PlaidAccessTokenResponse {..} <- ER.hoistEither (checkResult result' :: Either PlaidError PlaidAccessTokenResponse)
+        authBody <- ER.hoistEither (mkGetAuthEnv env _plaidAccessTokenResponseAccessToken)
+        let authGetResult = runTestPlaid $ runTestPostRequest "/auth/get" authBody
+        ER.hoistEither (checkResult authGetResult :: Either PlaidError PlaidAuthGetResponse)
+      isRight testResult `shouldBe` True
+
+  describe "Identity get" $ do
+    it "/identity/get" $ do
+      let result = runTestPlaid $ runTestPostRequest "/public_token/create" publicTokenBody
+      testResult <- ER.runExceptT $ do
+        PlaidPublicTokenResponse {..} <- ER.hoistEither (checkResult result :: Either PlaidError PlaidPublicTokenResponse)
+        exchangeBody <- ER.hoistEither (mkExchangePublicTokenEnv env _plaidPublicTokenResponsePublicToken)
+        let result' = runTestPlaid $ runTestPostRequest "/item/public_token/exchange" exchangeBody
+        PlaidAccessTokenResponse {..} <- ER.hoistEither (checkResult result' :: Either PlaidError PlaidAccessTokenResponse)
+        identityBody <- ER.hoistEither (mkCreateIdentityGetEnv env _plaidAccessTokenResponseAccessToken)
+        let identityGetResult = runTestPlaid $ runTestPostRequest "/identity/get" identityBody
+        ER.hoistEither (checkResult identityGetResult :: Either PlaidError PlaidIdentityGetResponse)
+      isRight testResult `shouldBe` True
+  describe "Transactions get" $ do
+    it "/transactions/get" $ do
+      let result = runTestPlaid $ runTestPostRequest "/public_token/create" publicTokenBody
+      testResult <- ER.runExceptT $ do
+        PlaidPublicTokenResponse {..} <- ER.hoistEither (checkResult result :: Either PlaidError PlaidPublicTokenResponse)
+        exchangeBody <- ER.hoistEither (mkExchangePublicTokenEnv env _plaidPublicTokenResponsePublicToken)
+        let result' = runTestPlaid $ runTestPostRequest "/item/public_token/exchange" exchangeBody
+        PlaidAccessTokenResponse {..} <- ER.hoistEither (checkResult result' :: Either PlaidError PlaidAccessTokenResponse)
+        authBody <- ER.hoistEither (mkGetAuthEnv env _plaidAccessTokenResponseAccessToken)
+        let authGetResult = runTestPlaid $ runTestPostRequest "/auth/get" authBody
+        PlaidAuthGetResponse {..} <- ER.hoistEither (checkResult authGetResult :: Either PlaidError PlaidAuthGetResponse)
+        let startTime = fromGregorian 2018 1 1
+        let endTime   = fromGregorian 2019 1 1
+        transactionBody <-
+          ER.hoistEither (mkCreateTransactionsGetEnv
+                           env
+                           _plaidAccessTokenResponseAccessToken
+                           startTime
+                           endTime
+                           Nothing
+                           Nothing
+                         )
+
+        let transactionsGetResult =
+              runTestPlaid $ runTestPostRequest "/transactions/get" transactionBody
+        ER.hoistEither (checkResult transactionsGetResult :: Either PlaidError PlaidTransactionsGetResponse)
+      isRight testResult `shouldBe` True
+  describe "Balance json" $ do
+    it "should be able to decode Balance" $ do
+      let result = eitherDecode' balanceJson :: Either String Balance
+      isRight result `shouldBe` True
+  describe "Account json" $ do
+    it "should be able to decode Account" $ do
+      let result = eitherDecode' accountJson :: Either String Account
+      isRight result `shouldBe` True
+  describe "Account list json" $ do
+    it "should be able to decode Account list" $ do
+      let result = eitherDecode' accountListJson :: Either String [Account]
+      isRight result `shouldBe` True
+  describe "PaymentMeta json" $ do
+    it "should be able to decode PaymentMeta" $ do
+      let result = eitherDecode' accountJson :: Either String PaymentMeta
+      isRight result `shouldBe` True
+  describe "Item json" $ do
+    it "should be able to decode Item" $ do
+      let result = eitherDecode' itemJson :: Either String Item
+      isRight result `shouldBe` True
+  describe "Transaction json" $ do
+    it "should be able to decode Transaction" $ do
+      let result = eitherDecode' transactionJson :: Either String Transaction
+      isRight result `shouldBe` True
+  describe "TransactionLocation json" $ do
+    it "should be able to decode TransactionLocation" $ do
+      let result = eitherDecode' transactionLocationJson :: Either String TransactionLocation
+      isRight result `shouldBe` True
+  describe "email json" $ do
+    it "should be able to decode Email" $ do
+      let result = eitherDecode' emailJson :: Either String Email
+      isRight result `shouldBe` True
+  describe "phone json" $ do
+    it "should be able to decode Phone" $ do
+      let result = eitherDecode' phoneNumberJson :: Either String PhoneNumber
+      isRight result `shouldBe` True
+  describe "address json" $ do
+    it "should be able to decode Addresses" $ do
+      let result = eitherDecode' addressesJson :: Either String Addresses
+      isRight result `shouldBe` True
+  describe "Accounts json" $ do
+    it "should be able to decode Accounts" $ do
+      let result = eitherDecode' accountsJson :: Either String Accounts
+      isRight result `shouldBe` True
+  describe "Owners json" $ do
+    it "should be able to decode Owners" $ do
+      let result = eitherDecode' ownersJson :: Either String Owners
+      isRight result `shouldBe` True
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
