diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,1 +1,10 @@
 # 0.1.0
+
+## Methods
+
+### New
+
+- `api.test`
+- `auth.test`
+- `channels.create`
+- `chat.postMessage`
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,5 +1,21 @@
 # Haskell bindings for the Slack web API
 
+[![][1]][0]
+
+[![][3]][2]
+[![][5]][4]
+
+[0]: https://circleci.com/gh/jpvillaisaza/slack-web
+[1]: https://circleci.com/gh/jpvillaisaza/slack-web.svg?style=svg
+[2]: https://www.stackage.org/lts/package/slack-web
+[3]: https://www.stackage.org/package/slack-web/badge/lts
+[4]: https://www.stackage.org/nightly/package/slack-web
+[5]: https://www.stackage.org/package/slack-web/badge/nightly
+
+- Hackage: <https://hackage.haskell.org/package/slack-web>
+
+- Slack web API: <https://api.slack.com/web>
+
 ## Example
 
 ```
@@ -11,25 +27,27 @@
 ```
 
 ```
-> manager <- Slack.mkManager
+> import Control.Monad.Reader
 ```
 
 ```
-> :set -XRecordWildCards
-> Slack.Cli{..} = Slack.mkCli
-apiTest :: ...
-authTest :: ...
-channelsCreate :: ...
-chatPostMessage :: ...
+> :set -XOverloadedStrings
 ```
 
 ```
-> Slack.run manager (apiTest Api.mkTestReq)
+> slackConfig <- Slack.mkSlackConfig token
+```
+
+```
+> flip runReaderT slackConfig (Slack.apiTest Api.mkTestReq)
 Right ...
 ```
 
 ```
-> :set -XOverloadedStrings
-> Slack.run manager (apiTest Api.mkTestReq { testReqFoo = Just "bar" })
+> flip runReaderT slackConfig (Slack.apiTest Api.mkTestReq { Api.testReqFoo = Just "bar" })
 Right ...
 ```
+
+## License
+
+Licensed under the MIT license. See [LICENSE.md](LICENSE.md).
diff --git a/slack-web.cabal b/slack-web.cabal
--- a/slack-web.cabal
+++ b/slack-web.cabal
@@ -1,5 +1,5 @@
 name: slack-web
-version: 0.1.0
+version: 0.2.0
 
 build-type: Simple
 cabal-version: >= 1.21
@@ -35,23 +35,62 @@
       Web.Slack.Auth
       Web.Slack.Channel
       Web.Slack.Chat
+      Web.Slack.Common
+      Web.Slack.Group
+      Web.Slack.Im
+      Web.Slack.MessageParser
+      Web.Slack.User
+  other-modules:
+      Web.Slack.Types
       Web.Slack.Util
   build-depends:
-      aeson >= 1.1 && < 1.2
-    , base >= 4.9 && < 4.10
-    , generics-sop >= 0.2 && < 0.3
+      aeson >= 1.0 && < 1.3
+    , base >= 4.9 && < 4.11
+    , containers
     , http-api-data >= 0.3 && < 0.4
     , http-client >= 0.5 && < 0.6
     , http-client-tls >= 0.3 && < 0.4
-    , servant >= 0.10 && < 0.11
-    , servant-client >= 0.10 && < 0.11
+    , servant >= 0.9 && < 0.12
+    , servant-client >= 0.9 && < 0.12
     , text >= 1.2 && < 1.3
     , transformers
+    , mtl
+    , time
+    , errors
+    , megaparsec
   default-language:
       Haskell2010
   ghc-options:
       -Wall
 
+-- the test suite doesn't depend on slack-web
+-- but rather has the src as an extra source directory.
+-- that allows us to refer to non exported modules from tests.
+test-suite tests
+  main-is:
+      Spec.hs
+  hs-source-dirs:
+      src, tests
+  type:
+      exitcode-stdio-1.0
+  other-modules:
+      Web.Slack.MessageParser
+      Web.Slack.MessageParserSpec
+      Web.Slack.Types
+  build-depends:
+      base >= 4.9 && < 4.11
+    , containers
+    , aeson
+    , errors
+    , hspec
+    , http-api-data
+    , text
+    , time
+    , megaparsec
+  default-language:
+    Haskell2010
+  ghc-options:
+       -Wall
 
 source-repository head
   type: git
diff --git a/src/Web/Slack.hs b/src/Web/Slack.hs
--- a/src/Web/Slack.hs
+++ b/src/Web/Slack.hs
@@ -1,7 +1,5 @@
 {-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE TypeFamilies #-}
 {-# LANGUAGE TypeOperators #-}
 
@@ -15,160 +13,550 @@
 ----------------------------------------------------------------------
 
 module Web.Slack
-  ( Cli(..)
-  , mkCli
-  , run
-  , mkManager
+  ( SlackConfig(..)
+  , mkSlackConfig
+  , apiTest
+  , authTest
+  , chatPostMessage
+  , channelsCreate
+  , channelsList
+  , channelsHistory
+  , groupsHistory
+  , groupsList
+  , historyFetchAll
+  , imHistory
+  , imList
+  , mpimList
+  , mpimHistory
+  , getUserDesc
+  , usersList
+  , authenticateReq
+  , Response
+  , HasManager
+  , HasToken
   )
   where
 
+-- aeson
+import Data.Aeson
+
 -- base
+import Control.Arrow ((&&&))
+import Data.Maybe
 import Data.Proxy (Proxy(..))
-import GHC.Generics (Generic)
 
--- generics-sop
-import qualified Generics.SOP (Generic)
+-- containers
+import qualified Data.Map as Map
 
+-- error
+import Control.Error (lastZ, isNothing)
+
 -- http-client
 import Network.HTTP.Client (Manager, newManager)
 
 -- http-client-tls
 import Network.HTTP.Client.TLS (tlsManagerSettings)
 
+-- mtl
+import Control.Monad.Reader
+
 -- servant
 import Servant.API
 
 -- servant-client
 import Servant.Client
-import Servant.Client.Generic
+import Servant.Common.Req (Req, appendToQueryString)
 
 -- slack-web
 import qualified Web.Slack.Api as Api
 import qualified Web.Slack.Auth as Auth
 import qualified Web.Slack.Channel as Channel
 import qualified Web.Slack.Chat as Chat
+import qualified Web.Slack.Common as Common
+import qualified Web.Slack.Im as Im
+import qualified Web.Slack.Group as Group
+import qualified Web.Slack.User as User
 
--- transformers
-import Control.Monad.IO.Class
+-- text
+import Data.Text (Text)
 
+class HasManager a where
+    getManager :: a -> Manager
 
+class HasToken a where
+    getToken :: a -> Text
+
+-- | Implements the 'HasManager' and 'HasToken' typeclasses.
+data SlackConfig
+  = SlackConfig
+  { slackConfigManager :: Manager
+  , slackConfigToken :: Text
+  }
+
+instance HasManager SlackConfig where
+    getManager = slackConfigManager
+instance HasToken SlackConfig where
+    getToken = slackConfigToken
+
+-- contains errors that can be returned by the slack API.
+-- constrast with 'SlackClientError' which additionally
+-- contains errors which occured during the network communication.
+data ResponseSlackError = ResponseSlackError Text
+  deriving (Eq, Show)
+
+type Response a =  Either Common.SlackClientError a
+
 -- |
+-- Internal type!
 --
+newtype ResponseJSON a = ResponseJSON (Either ResponseSlackError a)
+
+instance FromJSON a => FromJSON (ResponseJSON a) where
+    parseJSON = withObject "Response" $ \o -> do
+        ok <- o .: "ok"
+        ResponseJSON <$> if ok
+           then Right <$> parseJSON (Object o)
+           else Left . ResponseSlackError <$> o .: "error"
+
+
+-- |
 --
+--
 
 type Api =
     "api.test"
       :> ReqBody '[FormUrlEncoded] Api.TestReq
-      :> Post '[JSON] Api.TestRsp
+      :> Post '[JSON] (ResponseJSON Api.TestRsp)
   :<|>
     "auth.test"
-      :> ReqBody '[FormUrlEncoded] Auth.TestReq
-      :> Post '[JSON] Auth.TestRsp
+      :> AuthProtect "token"
+      :> Post '[JSON] (ResponseJSON Auth.TestRsp)
   :<|>
     "channels.create"
+      :> AuthProtect "token"
       :> ReqBody '[FormUrlEncoded] Channel.CreateReq
-      :> Post '[JSON] Channel.CreateRsp
+      :> Post '[JSON] (ResponseJSON Channel.CreateRsp)
   :<|>
+    "channels.history"
+      :> AuthProtect "token"
+      :> ReqBody '[FormUrlEncoded] Common.HistoryReq
+      :> Post '[JSON] (ResponseJSON Common.HistoryRsp)
+  :<|>
+    "channels.list"
+      :> AuthProtect "token"
+      :> ReqBody '[FormUrlEncoded] Channel.ListReq
+      :> Post '[JSON] (ResponseJSON Channel.ListRsp)
+  :<|>
     "chat.postMessage"
+      :> AuthProtect "token"
       :> ReqBody '[FormUrlEncoded] Chat.PostMsgReq
-      :> Post '[JSON] Chat.PostMsgRsp
+      :> Post '[JSON] (ResponseJSON Chat.PostMsgRsp)
+  :<|>
+    "groups.history"
+      :> AuthProtect "token"
+      :> ReqBody '[FormUrlEncoded] Common.HistoryReq
+      :> Post '[JSON] (ResponseJSON Common.HistoryRsp)
+  :<|>
+     "groups.list"
+      :> AuthProtect "token"
+      :> Post '[JSON] (ResponseJSON Group.ListRsp)
+  :<|>
+    "im.history"
+      :> AuthProtect "token"
+      :> ReqBody '[FormUrlEncoded] Common.HistoryReq
+      :> Post '[JSON] (ResponseJSON Common.HistoryRsp)
+  :<|>
+    "im.list"
+      :> AuthProtect "token"
+      :> Post '[JSON] (ResponseJSON Im.ListRsp)
+  :<|>
+    "mpim.list"
+      :> AuthProtect "token"
+      :> Post '[JSON] (ResponseJSON Group.ListRsp)
+  :<|>
+    "mpim.history"
+      :> AuthProtect "token"
+      :> ReqBody '[FormUrlEncoded] Common.HistoryReq
+      :> Post '[JSON] (ResponseJSON Common.HistoryRsp)
+  :<|>
+    "users.list"
+      :> AuthProtect "token"
+      :> Post '[JSON] (ResponseJSON User.ListRsp)
 
 
 -- |
 --
+-- Check API calling code.
 --
+-- <https://api.slack.com/methods/api.test>
 
-data Cli =
-  Cli
-    { -- |
-      --
-      -- Check API calling code.
-      --
-      -- <https://api.slack.com/methods/api.test>
+apiTest
+  :: (MonadReader env m, HasManager env, MonadIO m)
+  => Api.TestReq
+  -> m (Response Api.TestRsp)
+apiTest req = run (apiTest_ req)
 
-      apiTest
-        :: Api.TestReq
-        -> ClientM Api.TestRsp
+apiTest_
+  :: Api.TestReq
+  -> ClientM (ResponseJSON Api.TestRsp)
 
-      -- |
-      --
-      -- Check authentication and identity.
-      --
-      -- <https://api.slack.com/methods/auth.test>
+-- |
+--
+-- Check authentication and identity.
+--
+-- <https://api.slack.com/methods/auth.test>
 
-    , authTest
-        :: Auth.TestReq
-        -> ClientM Auth.TestRsp
+authTest
+  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)
+  => m (Response Auth.TestRsp)
+authTest = do
+  authR <- mkSlackAuthenticateReq
+  run (authTest_ authR)
 
-      -- |
-      --
-      -- Create a channel.
-      --
-      -- <https://api.slack.com/methods/channels.create>
+authTest_
+  :: AuthenticateReq (AuthProtect "token")
+  -> ClientM (ResponseJSON Auth.TestRsp)
 
-    , channelsCreate
-        :: Channel.CreateReq
-        -> ClientM Channel.CreateRsp
 
-      -- |
-      --
-      -- Send a message to a channel.
-      --
-      -- <https://api.slack.com/methods/chat.postMessage>
+-- |
+--
+-- Create a channel.
+--
+-- <https://api.slack.com/methods/channels.create>
 
-    , chatPostMessage
-        :: Chat.PostMsgReq
-        -> ClientM Chat.PostMsgRsp
+channelsCreate
+  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)
+  => Channel.CreateReq
+  -> m (Response Channel.CreateRsp)
+channelsCreate createReq = do
+  authR <- mkSlackAuthenticateReq
+  run (channelsCreate_ authR createReq)
 
-    }
-  deriving (Generic)
+channelsCreate_
+  :: AuthenticateReq (AuthProtect "token")
+  -> Channel.CreateReq
+  -> ClientM (ResponseJSON Channel.CreateRsp)
 
+-- |
+--
+-- Retrieve channel list.
+--
+-- <https://api.slack.com/methods/channels.list>
 
+channelsList
+  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)
+  => Channel.ListReq
+  -> m (Response Channel.ListRsp)
+channelsList listReq = do
+  authR <- mkSlackAuthenticateReq
+  run (channelsList_ authR listReq)
+
+channelsList_
+  :: AuthenticateReq (AuthProtect "token")
+  -> Channel.ListReq
+  -> ClientM (ResponseJSON Channel.ListRsp)
+
 -- |
 --
+-- Retrieve channel history.
+-- Consider using 'historyFetchAll' in combination with this function
 --
+-- <https://api.slack.com/methods/channels.history>
 
-instance Generics.SOP.Generic Cli
+channelsHistory
+  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)
+  => Common.HistoryReq
+  -> m (Response Common.HistoryRsp)
+channelsHistory histReq = do
+  authR <- mkSlackAuthenticateReq
+  run (channelsHistory_ authR histReq)
 
+channelsHistory_
+  :: AuthenticateReq (AuthProtect "token")
+  -> Common.HistoryReq
+  -> ClientM (ResponseJSON Common.HistoryRsp)
 
 -- |
 --
+-- Send a message to a channel.
 --
+-- <https://api.slack.com/methods/chat.postMessage>
 
-instance (Client Api ~ cli) => ClientLike cli Cli
+chatPostMessage
+  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)
+  => Chat.PostMsgReq
+  -> m (Response Chat.PostMsgRsp)
+chatPostMessage postReq = do
+  authR <- mkSlackAuthenticateReq
+  run (chatPostMessage_ authR postReq)
 
+chatPostMessage_
+  :: AuthenticateReq (AuthProtect "token")
+  -> Chat.PostMsgReq
+  -> ClientM (ResponseJSON Chat.PostMsgRsp)
 
 -- |
 --
+-- This method returns a list of private channels in the team that the caller
+-- is in and archived groups that the caller was in. The list of
+-- (non-deactivated) members in each private channel is also returned.
 --
+-- <https://api.slack.com/methods/groups.list>
 
-mkCli :: Cli
-mkCli =
-  mkClient (client (Proxy :: Proxy Api))
+groupsList
+  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)
+  => m (Response Group.ListRsp)
+groupsList = do
+  authR <- mkSlackAuthenticateReq
+  run (groupsList_ authR)
 
+groupsList_
+  :: AuthenticateReq (AuthProtect "token")
+  -> ClientM (ResponseJSON Group.ListRsp)
 
 -- |
 --
+-- This method returns a portion of messages/events from the specified
+-- private channel. To read the entire history for a private channel,
+-- call the method with no latest or oldest arguments, and then continue paging.
+-- Consider using 'historyFetchAll' in combination with this function
 --
+-- <https://api.slack.com/methods/groups.history>
 
-run
-  :: MonadIO m
-  => Manager
-  -> ClientM a
-  -> m (Either ServantError a)
-run manager =
-  let
-    baseUrl =
-      BaseUrl Https "slack.com" 443 "/api"
+groupsHistory
+  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)
+  => Common.HistoryReq
+  -> m (Response Common.HistoryRsp)
+groupsHistory hisReq = do
+  authR <- mkSlackAuthenticateReq
+  run (groupsHistory_ authR hisReq)
 
+groupsHistory_
+  :: AuthenticateReq (AuthProtect "token")
+  -> Common.HistoryReq
+  -> ClientM (ResponseJSON Common.HistoryRsp)
+
+-- |
+--
+-- Returns a list of all direct message channels that the user has
+--
+-- <https://api.slack.com/methods/im.list>
+
+imList
+  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)
+  => m (Response Im.ListRsp)
+imList = do
+  authR <- mkSlackAuthenticateReq
+  run (imList_ authR)
+
+imList_
+  :: AuthenticateReq (AuthProtect "token")
+  -> ClientM (ResponseJSON Im.ListRsp)
+
+-- |
+--
+-- Retrieve direct message channel history.
+-- Consider using 'historyFetchAll' in combination with this function
+--
+-- <https://api.slack.com/methods/im.history>
+
+imHistory
+  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)
+  => Common.HistoryReq
+  -> m (Response Common.HistoryRsp)
+imHistory histReq = do
+  authR <- mkSlackAuthenticateReq
+  run (imHistory_ authR histReq)
+
+imHistory_
+  :: AuthenticateReq (AuthProtect "token")
+  -> Common.HistoryReq
+  -> ClientM (ResponseJSON Common.HistoryRsp)
+
+-- |
+--
+-- Returns a list of all multiparty direct message channels that the user has
+--
+-- <https://api.slack.com/methods/mpim.list>
+
+mpimList
+  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)
+  => m (Response Group.ListRsp)
+mpimList = do
+  authR <- mkSlackAuthenticateReq
+  run (mpimList_ authR)
+
+mpimList_
+  :: AuthenticateReq (AuthProtect "token")
+  -> ClientM (ResponseJSON Group.ListRsp)
+
+-- |
+--
+-- Retrieve multiparty direct message channel history.
+-- Consider using 'historyFetchAll' in combination with this function
+--
+-- <https://api.slack.com/methods/mpim.history>
+
+mpimHistory
+  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)
+  => Common.HistoryReq
+  -> m (Response Common.HistoryRsp)
+mpimHistory histReq = do
+  authR <- mkSlackAuthenticateReq
+  run (mpimHistory_ authR histReq)
+
+mpimHistory_
+  :: AuthenticateReq (AuthProtect "token")
+  -> Common.HistoryReq
+  -> ClientM (ResponseJSON Common.HistoryRsp)
+
+-- |
+--
+-- This method returns a list of all users in the team.
+-- This includes deleted/deactivated users.
+--
+-- <https://api.slack.com/methods/users.list>
+
+usersList
+  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)
+  => m (Response User.ListRsp)
+usersList = do
+  authR <- mkSlackAuthenticateReq
+  run (usersList_ authR)
+
+usersList_
+  :: AuthenticateReq (AuthProtect "token")
+  -> ClientM (ResponseJSON User.ListRsp)
+
+-- | Returns a function to get a username from a 'Common.UserId'.
+-- Comes in handy to use 'Web.Slack.MessageParser.messageToHtml'
+getUserDesc
+  :: (Common.UserId -> Text)
+  -- ^ A function to give a default username in case the username is unknown
+  -> User.ListRsp
+  -- ^ List of users as known by the slack server. See 'usersList'.
+  -> (Common.UserId -> Text)
+  -- ^ A function from 'Common.UserId' to username.
+getUserDesc unknownUserFn users =
+  let userMap = Map.fromList $ (User.userId &&& User.userName) <$> User.listRspMembers users
   in
-    liftIO . flip runClientM (ClientEnv manager baseUrl)
+    \userId -> fromMaybe (unknownUserFn userId) $ Map.lookup userId userMap
 
+-- |
+-- Fetch all history items between two dates. The basic calls
+-- 'channelsHistory', 'groupsHistory', 'imHistory' and so on
+-- may not return exhaustive results if there were too many
+-- records. You need to use 'Web.Slack.Common.historyRspHasMore' to find out
+-- whether you got all the data.
+--
+-- This function will repeatedly call the underlying history
+-- function until all the data is fetched or until a call
+-- fails, merging the messages obtained from each call.
+historyFetchAll
+  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)
+  => (Common.HistoryReq -> m (Response Common.HistoryRsp))
+  -- ^ The request to make. Can be for instance 'mpimHistory', 'channelsHistory'...
+  -> Text
+  -- ^ The channel name to query
+  -> Int
+  -- ^ The number of entries to fetch at once.
+  -> Common.SlackTimestamp
+  -- ^ The oldest timestamp to fetch records from
+  -> Common.SlackTimestamp
+  -- ^ The newest timestamp to fetch records to
+  -> m (Response Common.HistoryRsp)
+  -- ^ A list merging all the history records that were fetched
+  -- through the individual queries.
+historyFetchAll makeReq channel count oldest latest = do
+    -- From slack apidoc: If there are more than 100 messages between
+    -- the two timestamps then the messages returned are the ones closest to latest.
+    -- In most cases an application will want the most recent messages
+    -- and will page backward from there.
+    --
+    -- for reference (does not apply here) => If oldest is provided but not
+    -- latest then the messages returned are those closest to oldest,
+    -- allowing you to page forward through history if desired.
+    rsp <- makeReq $ Common.HistoryReq channel count (Just latest) (Just oldest) False
+    case rsp of
+      Left _ -> return rsp
+      Right (Common.HistoryRsp msgs hasMore) -> do
+          let oldestReceived = Common.messageTs <$> lastZ msgs
+          if not hasMore || isNothing oldestReceived
+              then return rsp
+              else mergeResponses msgs <$>
+                   historyFetchAll makeReq channel count oldest (fromJust oldestReceived)
 
+mergeResponses
+  :: [Common.Message]
+  -> Response Common.HistoryRsp
+  -> Response Common.HistoryRsp
+mergeResponses _ err@(Left _) = err
+mergeResponses msgs (Right rsp) =
+    Right (rsp { Common.historyRspMessages = msgs ++ Common.historyRspMessages rsp })
+
+apiTest_
+  :<|> authTest_
+  :<|> channelsCreate_
+  :<|> channelsHistory_
+  :<|> channelsList_
+  :<|> chatPostMessage_
+  :<|> groupsHistory_
+  :<|> groupsList_
+  :<|> imHistory_
+  :<|> imList_
+  :<|> mpimList_
+  :<|> mpimHistory_
+  :<|> usersList_
+  =
+  client (Proxy :: Proxy Api)
+
+
 -- |
 --
 --
 
-mkManager :: IO Manager
-mkManager =
-  newManager tlsManagerSettings
+type instance AuthClientData (AuthProtect "token") =
+  Text
+
+
+-- |
+--
+--
+
+authenticateReq
+  :: Text
+  -> Req
+  -> Req
+authenticateReq token =
+  appendToQueryString "token" (Just token)
+
+
+-- |
+--
+--
+
+run
+  :: (MonadReader env m, HasManager env, MonadIO m)
+  => ClientM (ResponseJSON a)
+  -> m (Response a)
+run clientAction = do
+  env <- ask
+  let baseUrl = BaseUrl Https "slack.com" 443 "/api"
+  unnestErrors <$> liftIO (runClientM clientAction $ ClientEnv (getManager env) baseUrl)
+
+mkSlackAuthenticateReq :: (MonadReader env m, HasToken env)
+  => m (AuthenticateReq (AuthProtect "token"))
+mkSlackAuthenticateReq = flip mkAuthenticateReq authenticateReq . getToken <$> ask
+
+unnestErrors :: Either ServantError (ResponseJSON a) -> Response a
+unnestErrors (Right (ResponseJSON (Right a))) = Right a
+unnestErrors (Right (ResponseJSON (Left (ResponseSlackError serv))))
+    = Left (Common.SlackError serv)
+unnestErrors (Left slackErr) = Left (Common.ServantError slackErr)
+
+
+-- | Prepare a SlackConfig from a slack token.
+-- You can then call the other functions providing this in a reader context.
+--
+mkSlackConfig :: Text -> IO SlackConfig
+mkSlackConfig token = SlackConfig <$> newManager tlsManagerSettings <*> pure token
diff --git a/src/Web/Slack/Api.hs b/src/Web/Slack/Api.hs
--- a/src/Web/Slack/Api.hs
+++ b/src/Web/Slack/Api.hs
@@ -81,14 +81,11 @@
 
 data TestRsp =
   TestRsp
-    { testRspOk :: Bool
-    , testRspArgs :: Maybe TestReq
+    { testRspArgs :: Maybe TestReq
     }
   deriving (Eq, Generic, Show)
 
-
 -- |
 --
 --
-
-$(deriveJSON (jsonOpts "testRsp") ''TestRsp)
+$(deriveFromJSON (jsonOpts "testRsp") ''TestRsp)
diff --git a/src/Web/Slack/Auth.hs b/src/Web/Slack/Auth.hs
--- a/src/Web/Slack/Auth.hs
+++ b/src/Web/Slack/Auth.hs
@@ -21,9 +21,6 @@
 -- base
 import GHC.Generics (Generic)
 
--- http-api-data
-import Web.FormUrlEncoded
-
 -- slack-web
 import Web.Slack.Util
 
@@ -35,48 +32,9 @@
 --
 --
 
-newtype TestReq =
-  TestReq
-    { testReqToken :: Text
-    }
-  deriving (Eq, Generic, Show)
-
-
--- |
---
---
-
-$(deriveJSON (jsonOpts "testReq") ''TestReq)
-
-
--- |
---
---
-
-instance ToForm TestReq where
-  toForm =
-    genericToForm (formOpts "testReq")
-
-
--- |
---
---
-
-mkTestReq
-  :: Text
-  -> TestReq
-mkTestReq =
-  TestReq
-
-
--- |
---
---
-
 data TestRsp =
   TestRsp
-    { testRspOk :: Bool
-    , testRspUrl :: Text
+    { testRspUrl :: Text
     , testRspTeam :: Text
     , testRspUser :: Text
     , testRspTeamId :: Text
@@ -85,9 +43,5 @@
     }
   deriving (Eq, Generic, Show)
 
-
--- |
---
---
 
 $(deriveJSON (jsonOpts "testRsp") ''TestRsp)
diff --git a/src/Web/Slack/Channel.hs b/src/Web/Slack/Channel.hs
--- a/src/Web/Slack/Channel.hs
+++ b/src/Web/Slack/Channel.hs
@@ -18,6 +18,9 @@
   , CreateReq(..)
   , mkCreateReq
   , CreateRsp(..)
+  , ListReq(..)
+  , mkListReq
+  , ListRsp(..)
   )
   where
 
@@ -31,6 +34,7 @@
 import Web.FormUrlEncoded
 
 -- slack-web
+import Web.Slack.Common
 import Web.Slack.Util
 
 -- text
@@ -46,15 +50,15 @@
     { channelId :: Text
     , channelName :: Text
     , channelCreated :: Integer
-    , channelCreator :: Text
+    , channelCreator :: UserId
     , channelIsArchived :: Bool
     , channelIsMember :: Bool
     , channelIsGeneral :: Bool
-    , channelLastRead :: Text
+    , channelLastRead :: Maybe Text
     , channelLatest :: Maybe Text
-    , channelUnreadCount :: Integer
-    , channelUnreadCountDisplay :: Integer
-    , channelMembers :: [Text]
+    , channelUnreadCount :: Maybe Integer
+    , channelUnreadCountDisplay :: Maybe Integer
+    , channelMembers :: [UserId]
     , channelTopic :: Topic
     , channelPurpose :: Purpose
     }
@@ -91,7 +95,7 @@
 --
 --
 
-$(deriveJSON (jsonOpts "channel") ''Channel)
+$(deriveFromJSON (jsonOpts "channel") ''Channel)
 
 
 -- |
@@ -114,8 +118,7 @@
 
 data CreateReq =
   CreateReq
-    { createReqToken :: Text
-    , createReqName :: Text
+    { createReqName :: Text
     , createReqValidate :: Maybe Bool
     }
   deriving (Eq, Generic, Show)
@@ -143,12 +146,10 @@
 
 mkCreateReq
   :: Text
-  -> Text
   -> CreateReq
-mkCreateReq token name =
+mkCreateReq name =
   CreateReq
-    { createReqToken = token
-    , createReqName = name
+    { createReqName = name
     , createReqValidate = Nothing
     }
 
@@ -159,8 +160,7 @@
 
 data CreateRsp =
   CreateRsp
-    { createRspOk :: Bool
-    , createRspChannel :: Channel
+    { createRspChannel :: Channel
     }
   deriving (Eq, Generic, Show)
 
@@ -168,4 +168,53 @@
 -- |
 --
 --
-$(deriveJSON (jsonOpts "createRsp") ''CreateRsp)
+$(deriveFromJSON (jsonOpts "createRsp") ''CreateRsp)
+
+data ListReq =
+  ListReq
+    { listReqExcludeArchived :: Maybe Bool
+    , listReqExcludeMembers :: Maybe Bool
+    }
+  deriving (Eq, Generic, Show)
+
+
+-- |
+--
+--
+
+$(deriveJSON (jsonOpts "listReq") ''ListReq)
+
+
+-- |
+--
+--
+
+instance ToForm ListReq where
+  toForm =
+    genericToForm (formOpts "listReq")
+
+
+-- |
+--
+--
+
+mkListReq
+  :: ListReq
+mkListReq =
+  ListReq
+    { listReqExcludeArchived = Nothing
+    , listReqExcludeMembers = Nothing
+    }
+
+
+-- |
+--
+--
+
+data ListRsp =
+  ListRsp
+    { listRspChannels :: [Channel]
+    }
+  deriving (Eq, Generic, Show)
+
+$(deriveFromJSON (jsonOpts "listRsp") ''ListRsp)
diff --git a/src/Web/Slack/Chat.hs b/src/Web/Slack/Chat.hs
--- a/src/Web/Slack/Chat.hs
+++ b/src/Web/Slack/Chat.hs
@@ -66,8 +66,7 @@
 
 data PostMsgReq =
   PostMsgReq
-    { postMsgReqToken :: Text
-    , postMsgReqChannel :: Text
+    { postMsgReqChannel :: Text
     , postMsgReqText :: Text
     , postMsgReqParse :: Maybe Text
     , postMsgReqLinkNames :: Maybe Bool
@@ -107,12 +106,10 @@
 mkPostMsgReq
   :: Text
   -> Text
-  -> Text
   -> PostMsgReq
-mkPostMsgReq token channel text =
+mkPostMsgReq channel text =
   PostMsgReq
-    { postMsgReqToken = token
-    , postMsgReqChannel = channel
+    { postMsgReqChannel = channel
     , postMsgReqText = text
     , postMsgReqParse = Nothing
     , postMsgReqLinkNames = Nothing
@@ -134,15 +131,9 @@
 
 data PostMsgRsp =
   PostMsgRsp
-    { postMsgRspOk :: Bool
-    , postMsgRspTs :: String
+    { postMsgRspTs :: String
     , postMsgRspMessage :: PostMsg
     }
   deriving (Eq, Generic, Show)
 
-
--- |
---
---
-
-$(deriveJSON (jsonOpts "postMsgRsp") ''PostMsgRsp)
+$(deriveFromJSON (jsonOpts "postMsgRsp") ''PostMsgRsp)
diff --git a/src/Web/Slack/Common.hs b/src/Web/Slack/Common.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Slack/Common.hs
@@ -0,0 +1,143 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE OverloadedLists #-}
+
+----------------------------------------------------------------------
+-- |
+-- Module: Web.Slack.Common
+-- Description:
+--
+--
+--
+----------------------------------------------------------------------
+
+module Web.Slack.Common
+  ( Color(unColor)
+  , UserId(unUserId)
+  , SlackTimestamp(..)
+  , mkSlackTimestamp
+  , HistoryReq(..)
+  , mkHistoryReq
+  , HistoryRsp(..)
+  , Message(..)
+  , MessageType(..)
+  , SlackClientError(..)
+  , SlackMessageText(..)
+  )
+  where
+
+-- aeson
+import Data.Aeson
+import Data.Aeson.TH
+
+-- base
+import GHC.Generics (Generic)
+
+-- http-api-data
+import Web.HttpApiData
+import Web.FormUrlEncoded
+  
+-- servant-client
+import Servant.Common.Req
+
+-- slack-web
+import Web.Slack.Types
+import Web.Slack.Util
+
+-- text
+import Data.Text (Text)
+
+
+-- |
+--
+--
+
+data HistoryReq =
+  HistoryReq
+    { historyReqChannel :: Text
+    , historyReqCount :: Int
+    , historyReqLatest :: Maybe SlackTimestamp
+    , historyReqOldest :: Maybe SlackTimestamp
+    , historyReqInclusive :: Bool
+    }
+  deriving (Eq, Generic, Show)
+
+
+-- |
+--
+--
+
+$(deriveFromJSON (jsonOpts "historyReq") ''HistoryReq)
+
+
+-- |
+--
+--
+
+instance ToForm HistoryReq where
+  -- can't use genericToForm because slack expects booleans as 0/1
+  toForm HistoryReq{..} =
+      [ ("channel", toQueryParam historyReqChannel)
+      , ("count", toQueryParam historyReqCount)
+      , ("latest", toQueryParam historyReqLatest)
+      , ("oldest", toQueryParam historyReqOldest)
+      , ("inclusive", toQueryParam (if historyReqInclusive then 1::Int else 0))
+      ]
+
+
+-- |
+--
+--
+
+mkHistoryReq
+  :: Text
+  -> HistoryReq
+mkHistoryReq channel =
+  HistoryReq
+    { historyReqChannel = channel
+    , historyReqCount = 100
+    , historyReqLatest = Nothing
+    , historyReqOldest = Nothing
+    , historyReqInclusive = True
+    }
+
+data MessageType = MessageTypeMessage
+  deriving (Eq, Show)
+
+instance FromJSON MessageType where
+  parseJSON "message" = pure MessageTypeMessage
+  parseJSON _ = fail "Invalid MessageType"
+
+data Message =
+  Message
+    { messageType :: MessageType
+    , messageUser :: Maybe UserId -- ^ not present for bot messages at least
+    , messageText :: SlackMessageText
+    -- ^ the message text is in a markdown-like slack-specific format.
+    -- Use 'Web.Slack.MessageParser.messageToHtml' to convert it to HTML.
+    , messageTs :: SlackTimestamp
+    }
+  deriving (Eq, Generic, Show)
+
+$(deriveFromJSON (jsonOpts "message") ''Message)
+
+data HistoryRsp =
+  HistoryRsp
+    { historyRspMessages :: [Message]
+    , historyRspHasMore :: Bool
+    }
+  deriving (Eq, Generic, Show)
+
+$(deriveFromJSON (jsonOpts "historyRsp") ''HistoryRsp)
+
+-- |
+-- Errors that can be triggered by a slack request.
+data SlackClientError
+    = ServantError ServantError
+    -- ^ errors from the network connection
+    | SlackError Text
+    -- ^ errors returned by the slack API
+  deriving (Eq, Generic, Show)
diff --git a/src/Web/Slack/Group.hs b/src/Web/Slack/Group.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Slack/Group.hs
@@ -0,0 +1,56 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+----------------------------------------------------------------------
+-- |
+-- Module: Web.Slack.Group
+-- Description:
+--
+--
+--
+----------------------------------------------------------------------
+
+module Web.Slack.Group
+  ( Group(..)
+  , ListRsp(..)
+  )
+  where
+
+-- aeson
+import Data.Aeson.TH
+
+-- base
+import GHC.Generics (Generic)
+
+-- slack-web
+import Web.Slack.Common
+import Web.Slack.Util
+
+-- text
+import Data.Text (Text)
+
+-- time
+import Data.Time.Clock.POSIX
+
+data Group =
+  Group
+    { groupId :: Text
+    , groupName :: Text
+    , groupIsMpim :: Bool
+    , groupCreated :: POSIXTime
+    , groupCreator :: UserId
+    , groupIsArchived :: Bool
+    , groupMembers :: [UserId]
+    }
+  deriving (Eq, Generic, Show)
+
+$(deriveFromJSON (jsonOpts "group") ''Group)
+
+data ListRsp =
+  ListRsp
+    { listRspGroups :: [Group]
+    }
+  deriving (Eq, Generic, Show)
+
+$(deriveFromJSON (jsonOpts "listRsp") ''ListRsp)
diff --git a/src/Web/Slack/Im.hs b/src/Web/Slack/Im.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Slack/Im.hs
@@ -0,0 +1,55 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeApplications #-}
+
+----------------------------------------------------------------------
+-- |
+-- Module: Web.Slack.Im
+-- Description:
+--
+--
+--
+----------------------------------------------------------------------
+
+module Web.Slack.Im
+  ( Im(..)
+  , ListRsp(..)
+  )
+  where
+
+-- aeson
+import Data.Aeson.TH
+
+-- base
+import GHC.Generics (Generic)
+
+-- slack-web
+import Web.Slack.Util
+import Web.Slack.Common
+
+-- text
+import Data.Text (Text)
+
+-- time
+import Data.Time.Clock.POSIX
+
+data Im =
+  Im
+    { imId :: Text
+    , imIsIm :: Bool
+    , imUser :: UserId
+    , imCreated :: POSIXTime
+    , imIsUserDeleted :: Bool
+    }
+  deriving (Eq, Generic, Show)
+
+$(deriveFromJSON (jsonOpts "im") ''Im)
+
+data ListRsp =
+  ListRsp
+    { listRspIms :: [Im]
+    }
+  deriving (Eq, Generic, Show)
+
+$(deriveFromJSON (jsonOpts "listRsp") ''ListRsp)
diff --git a/src/Web/Slack/MessageParser.hs b/src/Web/Slack/MessageParser.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Slack/MessageParser.hs
@@ -0,0 +1,190 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE RecordWildCards #-}
+
+-- | See https://api.slack.com/docs/message-formatting
+--
+module Web.Slack.MessageParser
+  ( messageToHtml
+  , HtmlRenderers(..)
+  , defaultHtmlRenderers
+  )
+  where
+
+-- base
+import Control.Monad
+import Data.List
+import Data.Maybe
+import Data.Monoid
+
+-- megaparsec
+import Text.Megaparsec
+
+-- mtl
+import Data.Functor.Identity
+
+-- slack-web
+import Web.Slack.Types
+
+-- text
+import Data.Text (Text)
+import qualified Data.Text as T
+
+newtype SlackUrl = SlackUrl { unSlackUrl :: Text }
+  deriving (Show, Eq)
+
+data SlackMsgItem
+  = SlackMsgItemPlainText Text
+  | SlackMsgItemBoldSection [SlackMsgItem]
+  | SlackMsgItemItalicsSection [SlackMsgItem]
+  | SlackMsgItemStrikethroughSection [SlackMsgItem]
+  | SlackMsgItemLink Text SlackUrl
+  | SlackMsgItemUserLink UserId (Maybe Text)
+  | SlackMsgItemInlineCodeSection Text
+  | SlackMsgItemCodeSection Text
+  | SlackMsgItemQuoted [SlackMsgItem]
+  | SlackMsgItemEmoticon Text
+  deriving (Show, Eq)
+
+type SlackParser a = ParsecT Dec T.Text Identity a
+
+parseMessage :: Text -> [SlackMsgItem]
+parseMessage input = fromMaybe [SlackMsgItemPlainText input] $
+  parseMaybe (some $ parseMessageItem True) input
+
+parseMessageItem :: Bool -> SlackParser SlackMsgItem
+parseMessageItem acceptNewlines
+  = parseBoldSection
+  <|> parseItalicsSection
+  <|> parseStrikethroughSection
+  <|> try parseEmoticon
+  <|> parseCode
+  <|> parseInlineCode
+  <|> parseUserLink
+  <|> parseLink
+  <|> parseBlockQuote
+  <|> parsePlainText
+  <|> parseWhitespace acceptNewlines
+
+parsePlainText :: SlackParser SlackMsgItem
+parsePlainText = SlackMsgItemPlainText . T.pack <$>
+    someTill (noneOf stopChars) (void (lookAhead $ try $ oneOf stopChars)
+                                   <|> lookAhead (try $ choice $ sectionEndSymbol <$> ['*', '_', ':', '~'])
+                                   <|> lookAhead eof)
+    where stopChars = [' ', '\n']
+
+-- slack accepts bold/italics modifiers
+-- only at word boundary. for instance 'my_word'
+-- doesn't trigger an italics section.
+parseWhitespace :: Bool -> SlackParser SlackMsgItem
+parseWhitespace True =
+  SlackMsgItemPlainText . T.pack <$> some (oneOf [' ', '\n'])
+parseWhitespace False = SlackMsgItemPlainText . T.pack <$> some (oneOf [' '])
+
+sectionEndSymbol :: Char -> SlackParser ()
+sectionEndSymbol chr = void $ char chr >> lookAhead wordBoundary
+
+parseCharDelimitedSection :: Char -> SlackParser [SlackMsgItem]
+parseCharDelimitedSection chr = 
+  char chr *> someTill (parseMessageItem False) (sectionEndSymbol chr)
+
+wordBoundary :: SlackParser ()
+wordBoundary = void (oneOf [' ', '\n', '*', '_', ',', '`', '?', '!', ':', ';', '.']) <|> eof
+
+parseBoldSection :: SlackParser SlackMsgItem
+parseBoldSection =
+  fmap SlackMsgItemBoldSection (parseCharDelimitedSection '*')
+
+parseItalicsSection :: SlackParser SlackMsgItem
+parseItalicsSection =
+  fmap SlackMsgItemItalicsSection (parseCharDelimitedSection '_')
+
+parseStrikethroughSection :: SlackParser SlackMsgItem
+parseStrikethroughSection =
+  fmap SlackMsgItemStrikethroughSection (parseCharDelimitedSection '~')
+
+parseEmoticon :: SlackParser SlackMsgItem
+parseEmoticon = fmap (SlackMsgItemEmoticon . T.pack) $
+  char ':' *> someTill (alphaNumChar <|> char '_' <|> char '+') (sectionEndSymbol ':')
+
+parseUserLink :: SlackParser SlackMsgItem
+parseUserLink = do
+  void (string "<@")
+  userId <- UserId . T.pack <$> some (noneOf ['|', '>'])
+  let linkWithoutDesc = char '>' >>
+          pure (SlackMsgItemUserLink userId Nothing)
+  let linkWithDesc = char '|' >>
+          SlackMsgItemUserLink <$> pure userId <*> (Just <$> ((T.pack <$> some (noneOf ['>'])) <* char '>'))
+  linkWithDesc <|> linkWithoutDesc
+
+parseLink :: SlackParser SlackMsgItem
+parseLink = do
+  void (char '<')
+  url <- SlackUrl . T.pack <$> some (noneOf ['|', '>'])
+  let linkWithoutDesc = char '>' >>
+          pure (SlackMsgItemLink (unSlackUrl url) url)
+  let linkWithDesc = char '|' >>
+          SlackMsgItemLink <$> ((T.pack <$> some (noneOf ['>'])) <* char '>') <*> pure url
+  linkWithDesc <|> linkWithoutDesc
+
+parseCode :: SlackParser SlackMsgItem
+parseCode = SlackMsgItemCodeSection . T.pack <$>
+  (string "```" >> manyTill anyChar (string "```"))
+
+parseInlineCode :: SlackParser SlackMsgItem
+parseInlineCode = SlackMsgItemInlineCodeSection . T.pack <$>
+  (char '`' *> some (noneOf ['`']) <* char '`')
+
+parseBlockQuote :: SlackParser SlackMsgItem
+parseBlockQuote = SlackMsgItemQuoted . intercalate [SlackMsgItemPlainText "<br/>"] <$> some blockQuoteLine
+
+blockQuoteLine :: SlackParser [SlackMsgItem]
+blockQuoteLine = string "&gt;" *> optional (char ' ') *>
+    manyTill (parseMessageItem False) (eof <|> void newline)
+
+-- |
+-- Convert the slack format for messages (markdown like, see
+-- https://api.slack.com/docs/message-formatting ) to HTML.
+messageToHtml
+  :: HtmlRenderers
+  -- ^ Renderers allow you to customize the message rendering.
+  -- Give 'defaultHtmlRenderers' for a default implementation.
+  -> (UserId -> Text)
+  -- ^ A function giving a user name for a user id. You can use 'Web.Slack.getUserDesc'
+  -> SlackMessageText
+  -- ^ A slack message to convert to HTML
+  -> Text
+  -- ^ The HTML-formatted slack message
+messageToHtml htmlRenderers getUserDesc =
+  messageToHtml' htmlRenderers getUserDesc . parseMessage . unSlackMessageText
+
+messageToHtml' :: HtmlRenderers -> (UserId -> Text) -> [SlackMsgItem] -> Text
+messageToHtml' htmlRenderers getUserDesc = foldr ((<>) . msgItemToHtml htmlRenderers getUserDesc) ""
+
+data HtmlRenderers
+  = HtmlRenderers
+  { emoticonRenderer :: Text -> Text
+  }
+
+defaultHtmlRenderers :: HtmlRenderers
+defaultHtmlRenderers = HtmlRenderers
+  { emoticonRenderer = \code -> ":" <> code <> ":"
+  }
+  
+msgItemToHtml :: HtmlRenderers -> (UserId -> Text) -> SlackMsgItem -> Text
+msgItemToHtml htmlRenderers@HtmlRenderers{..} getUserDesc = \case
+  SlackMsgItemPlainText txt -> T.replace "\n" "<br/>" txt
+  SlackMsgItemBoldSection cts ->
+    "<b>" <> messageToHtml' htmlRenderers getUserDesc cts <> "</b>"
+  SlackMsgItemItalicsSection cts ->
+    "<i>" <> messageToHtml' htmlRenderers getUserDesc cts <> "</i>"
+  SlackMsgItemStrikethroughSection cts ->
+    "<strike>" <> messageToHtml' htmlRenderers getUserDesc cts <> "</strike>"
+  SlackMsgItemLink txt url ->
+    "<a href='" <> unSlackUrl url <> "'>" <> txt <> "</a>"
+  SlackMsgItemUserLink userId mTxt -> "@" <> fromMaybe (getUserDesc userId) mTxt
+  SlackMsgItemEmoticon code -> emoticonRenderer code
+  SlackMsgItemInlineCodeSection code -> "<code>" <> code <> "</code>"
+  SlackMsgItemCodeSection code -> "<pre>" <> code <> "</pre>"
+  SlackMsgItemQuoted items ->
+    "<blockquote>" <> messageToHtml' htmlRenderers getUserDesc items <> "</blockquote>"
diff --git a/src/Web/Slack/Types.hs b/src/Web/Slack/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Slack/Types.hs
@@ -0,0 +1,85 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE TypeApplications #-}
+
+----------------------------------------------------------------------
+-- |
+-- Module: Web.Slack.Types
+-- Description:
+--
+--
+--
+----------------------------------------------------------------------
+
+module Web.Slack.Types
+  ( Color(..)
+  , UserId(..)
+  , SlackTimestamp(..)
+  , mkSlackTimestamp
+  , SlackMessageText(..)
+  )
+  where
+
+-- aeson
+import Data.Aeson
+
+-- base
+import Data.Monoid
+import GHC.Generics (Generic)
+
+-- errors
+import Control.Error (hush)
+
+-- http-api-data
+import Web.HttpApiData
+
+-- text
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Text.Read (decimal)
+
+-- time
+import Data.Time.Clock
+import Data.Time.Clock.POSIX
+
+-- Ord to allow it to be a key of a Map
+newtype Color = Color { unColor :: Text }
+  deriving (Eq, Ord, Generic, Show, FromJSON)
+
+-- Ord to allow it to be a key of a Map
+newtype UserId = UserId { unUserId :: Text }
+  deriving (Eq, Ord, Generic, Show, FromJSON)
+
+-- | Message text in the format returned by Slack,
+-- see https://api.slack.com/docs/message-formatting
+-- Consider using 'messageToHtml' for displaying.
+newtype SlackMessageText = SlackMessageText { unSlackMessageText :: Text}
+  deriving (Eq, Generic, Show, FromJSON)
+
+data SlackTimestamp =
+  SlackTimestamp
+    { slackTimestampTs :: Text
+    , slackTimestampTime :: UTCTime
+    }
+  deriving (Eq, Show)
+
+instance Ord SlackTimestamp where
+    compare (SlackTimestamp _ a) (SlackTimestamp _ b) = compare a b
+
+mkSlackTimestamp :: UTCTime -> SlackTimestamp
+mkSlackTimestamp utctime = SlackTimestamp (T.pack (show @Integer unixts) <> ".000000") utctime
+  where unixts = floor $ toRational $ utcTimeToPOSIXSeconds utctime
+
+instance ToHttpApiData SlackTimestamp where
+  toQueryParam (SlackTimestamp contents _) = contents
+
+instance FromJSON SlackTimestamp where
+  parseJSON = withText "Slack ts" $ \contents ->
+    maybe (fail "Invalid slack ts")
+          (pure . SlackTimestamp contents)
+          (slackTimestampToTime contents)
+
+slackTimestampToTime :: Text -> Maybe UTCTime
+slackTimestampToTime txt =
+  posixSecondsToUTCTime . realToFrac @Integer . fst <$> hush (decimal txt)
diff --git a/src/Web/Slack/User.hs b/src/Web/Slack/User.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Slack/User.hs
@@ -0,0 +1,59 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+----------------------------------------------------------------------
+-- |
+-- Module: Web.Slack.User
+-- Description:
+--
+--
+--
+----------------------------------------------------------------------
+
+module Web.Slack.User
+  ( User(..)
+  , ListRsp(..)
+  )
+  where
+
+-- aeson
+import Data.Aeson.TH
+
+-- base
+import GHC.Generics (Generic)
+
+-- slack-web
+import Web.Slack.Common
+import Web.Slack.Util
+
+-- text
+import Data.Text (Text)
+
+-- time
+import Data.Time.Clock.POSIX
+
+data User =
+  User
+    { userId :: UserId
+    , userName :: Text
+    , userDeleted :: Bool
+    , userColor :: Maybe Color
+    , userIsAdmin :: Maybe Bool
+    , userIsOwner :: Maybe Bool
+    , userIsPrimaryOwner :: Maybe Bool
+    , userIsRestricted :: Maybe Bool
+    , userIsUltraRestricted :: Maybe Bool
+    , userUpdated :: POSIXTime
+    }
+  deriving (Eq, Generic, Show)
+
+$(deriveFromJSON (jsonOpts "user") ''User)
+
+data ListRsp =
+  ListRsp
+    { listRspMembers :: [User]
+    }
+  deriving (Eq, Generic, Show)
+
+$(deriveFromJSON (jsonOpts "listRsp") ''ListRsp)
diff --git a/src/Web/Slack/Util.hs b/src/Web/Slack/Util.hs
--- a/src/Web/Slack/Util.hs
+++ b/src/Web/Slack/Util.hs
@@ -15,6 +15,7 @@
 
 -- aeson
 import Data.Aeson.TH
+import Data.Aeson.Types
 
 -- base
 import Data.Char
@@ -73,12 +74,4 @@
   :: String
   -> String
 addUnderscores =
-  let
-    go res [] = res
-    go [] (x:xs) = go [toLower x] xs
-    go res (x:xs)
-      | isUpper x = go (toLower x : '_' : res) xs
-      | otherwise = go (x : res) xs
-
-  in
-    reverse . go []
+  camelTo2 '_'
diff --git a/tests/Spec.hs b/tests/Spec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/tests/Web/Slack/MessageParserSpec.hs b/tests/Web/Slack/MessageParserSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Web/Slack/MessageParserSpec.hs
@@ -0,0 +1,69 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Web.Slack.MessageParserSpec (spec) where
+
+-- base
+import Data.Monoid
+
+-- hspec
+import Test.Hspec
+
+-- slack-web
+import Web.Slack.MessageParser
+import Web.Slack.Types
+
+-- text
+import Data.Text (Text)
+
+testGetUserDesc :: UserId -> Text
+testGetUserDesc (UserId "USER1") = "user_one"
+testGetUserDesc x = unUserId x
+
+testHtmlRenderers :: HtmlRenderers
+testHtmlRenderers = HtmlRenderers
+  { emoticonRenderer = \x -> ":>" <> x <> "<:"
+  }
+
+msgToHtml :: Text -> Text
+msgToHtml = messageToHtml testHtmlRenderers testGetUserDesc . SlackMessageText
+
+spec :: Spec
+spec =
+  describe "message contents parsing" $ do
+    it "handles the trivial case well" $
+      msgToHtml "hello" `shouldBe` "hello"
+    it "converts a simple message to HTML correctly" $
+      msgToHtml "_hello_ *world* <http://www.google.com|google> `code` ```longer\ncode```"
+        `shouldBe`
+        "<i>hello</i> <b>world</b> <a href='http://www.google.com'>google</a> <code>code</code> <pre>longer\ncode</pre>"
+    it "degrades properly to return the input message if it's incorrect" $
+      msgToHtml "link not closed <bad"
+        `shouldBe`
+        "link not closed <bad"
+    it "creates italics sections only at word boundaries" $
+      msgToHtml "false_positive" `shouldBe` "false_positive"
+    it "handles bold, strikethrough & italics simultaneously" $
+      msgToHtml "*_~both~_*" `shouldBe` "<b><i><strike>both</strike></i></b>"
+    it "aborts nicely on interspersed bold & italics" $
+      msgToHtml "inter *sper_ *sed_" `shouldBe` "inter *sper_ *sed_"
+    it "parses blockquotes properly" $
+      msgToHtml "look at this:\n&gt; test *wow*" `shouldBe` "look at this:<br/><blockquote>test <b>wow</b></blockquote>"
+    it "parses code blocks properly" $
+      msgToHtml "look at this:\n```test *wow*```" `shouldBe` "look at this:<br/><pre>test *wow*</pre>"
+    it "handles non-italics underscores in text well" $
+      -- need to put other HTML symbols, otherwise if the parsing fails
+      -- i won't find out since we default to returning the input on
+      -- parsing failure
+      msgToHtml "a:\n&gt;b.\n:slightly_smiling_face:" `shouldBe` "a:<br/><blockquote>b.</blockquote>:>slightly_smiling_face<:"
+    it "properly parses multiline blockquotes" $
+      msgToHtml "&gt; first row\n&gt; second row\nthird row\n&gt; fourth row"
+        `shouldBe`
+        "<blockquote>first row<br/>second row</blockquote>third row<br/><blockquote>fourth row</blockquote>"
+    it "converts usernames" $
+      msgToHtml "<@USER1> should be converted, <@USER1|default> stay default"
+        `shouldBe`
+        "@user_one should be converted, @default stay default"
+    it "converts carriage returns" $
+      msgToHtml "a\nb" `shouldBe` "a<br/>b"
+    it "handles full stops as punctuation" $
+      msgToHtml "*b*." `shouldBe` "<b>b</b>."
