diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,12 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
+and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+## [0.1.1] - 2018-08-25
+
+- Intial release @domenkozar
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright 2018 Domen Kožar
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/README.lhs b/README.lhs
new file mode 100644
--- /dev/null
+++ b/README.lhs
@@ -0,0 +1,64 @@
+# MixPanel client for Haskell
+
+
+[![Build Status](https://travis-ci.com/domenkozar/mixpanel-client.svg?branch=master)](https://travis-ci.com/domenkozar/mixpanel-client)
+[![Hackage](https://img.shields.io/hackage/v/mixpanel-client.svg)](https://hackage.haskell.org/package/mixpanel-client)
+
+
+Implements major features of [MixPanel HTTP API](https://mixpanel.com/help/reference/http):
+
+- [x] track
+- [x] alias
+- [x] engage
+- [ ] import
+- [ ] export
+
+## Getting started
+
+```haskell
+import Data.Aeson      ( (.=) )
+import Data.Time.Clock ( getCurrentTime )
+import GHC.Exts        ( fromList )
+import MixPanel        ( Operation(Set), engage, track, alias
+                       , AuthToken(..), mkEnv
+                       -- reexports
+                       , newManager, tlsManagerSettings)
+
+
+main :: IO ()
+main = do
+  -- setup
+  manager <- newManager tlsManagerSettings
+  let env = mkEnv (AuthToken "foobar") manager
+
+  -- track a simple event
+  Right () <- track env "Played Video" mempty
+
+  -- track an event with extra properties
+  Right () <- track env "Played Video Unique per user"
+    $ fromList [ "distinct_id" .= ("generated-id" :: String)
+               , "customProperty" .= True ]
+
+  -- alias the user
+  Right () <- alias env "generated-id" "user@example.com"
+
+  -- profile engagement
+  now <- getCurrentTime
+  Right () <- engage env "user@example.com"
+    $ Set (fromList [ "$created" .= now])
+
+  putStrLn "All good!"
+```
+
+## Design questions
+
+a) Why does it use `Object/Array` intemediate values from `Data.Aeson`?
+
+
+## TODO
+
+- batch requests
+- expose extra url query paramerers
+- engage special properties more type safe?
+- filter out null values in requests to mixpanel
+- api support for tracking revenue
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,64 @@
+# MixPanel client for Haskell
+
+
+[![Build Status](https://travis-ci.com/domenkozar/mixpanel-client.svg?branch=master)](https://travis-ci.com/domenkozar/mixpanel-client)
+[![Hackage](https://img.shields.io/hackage/v/mixpanel-client.svg)](https://hackage.haskell.org/package/mixpanel-client)
+
+
+Implements major features of [MixPanel HTTP API](https://mixpanel.com/help/reference/http):
+
+- [x] track
+- [x] alias
+- [x] engage
+- [ ] import
+- [ ] export
+
+## Getting started
+
+```haskell
+import Data.Aeson      ( (.=) )
+import Data.Time.Clock ( getCurrentTime )
+import GHC.Exts        ( fromList )
+import MixPanel        ( Operation(Set), engage, track, alias
+                       , AuthToken(..), mkEnv
+                       -- reexports
+                       , newManager, tlsManagerSettings)
+
+
+main :: IO ()
+main = do
+  -- setup
+  manager <- newManager tlsManagerSettings
+  let env = mkEnv (AuthToken "foobar") manager
+
+  -- track a simple event
+  Right () <- track env "Played Video" mempty
+
+  -- track an event with extra properties
+  Right () <- track env "Played Video Unique per user"
+    $ fromList [ "distinct_id" .= ("generated-id" :: String)
+               , "customProperty" .= True ]
+
+  -- alias the user
+  Right () <- alias env "generated-id" "user@example.com"
+
+  -- profile engagement
+  now <- getCurrentTime
+  Right () <- engage env "user@example.com"
+    $ Set (fromList [ "$created" .= now])
+
+  putStrLn "All good!"
+```
+
+## Design questions
+
+a) Why does it use `Object/Array` intemediate values from `Data.Aeson`?
+
+
+## TODO
+
+- batch requests
+- expose extra url query paramerers
+- engage special properties more type safe?
+- filter out null values in requests to mixpanel
+- api support for tracking revenue
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/mixpanel-client.cabal b/mixpanel-client.cabal
new file mode 100644
--- /dev/null
+++ b/mixpanel-client.cabal
@@ -0,0 +1,103 @@
+-- This file has been generated from package.yaml by hpack version 0.28.2.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 491a6eeca6a6f94f2fbd7939dffe3bdf350c15ec0a7287c81ade08416e93c4a6
+
+name:           mixpanel-client
+version:        0.1.0.0
+synopsis:       Mixpanel client
+homepage:       https://github.com/domenkozar/mixpanel-client#readme
+bug-reports:    https://github.com/domenkozar/mixpanel-client/issues
+author:         Domen Kožar
+maintainer:     domen@enlambda.com
+copyright:      2018 Domen Kožar
+license:        Apache-2.0
+license-file:   LICENSE
+build-type:     Simple
+cabal-version:  >= 1.10
+extra-source-files:
+    ChangeLog.md
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/domenkozar/mixpanel-client
+
+library
+  exposed-modules:
+      MixPanel
+      MixPanel.API
+      MixPanel.Types.Core
+      MixPanel.Types.EngageData
+      MixPanel.Types.TrackData
+  other-modules:
+      Paths_mixpanel_client
+  hs-source-dirs:
+      src
+  default-extensions: OverloadedStrings LambdaCase DeriveGeneric DeriveFunctor DeriveAnyClass RecordWildCards
+  ghc-options: -Wall -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures -fwarn-name-shadowing -fwarn-incomplete-patterns
+  build-depends:
+      aeson
+    , base >=4.7 && <5
+    , base64-bytestring
+    , bytestring
+    , http-client
+    , http-client-tls
+    , servant >=0.14.1
+    , servant-client
+    , string-conv
+    , text
+    , time
+  default-language: Haskell2010
+
+test-suite mixpanel-test
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  other-modules:
+      Spec
+      TypesSpec
+      Paths_mixpanel_client
+  hs-source-dirs:
+      test
+  default-extensions: OverloadedStrings LambdaCase DeriveGeneric DeriveFunctor DeriveAnyClass RecordWildCards
+  ghc-options: -Wall -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures -fwarn-name-shadowing -fwarn-incomplete-patterns
+  build-depends:
+      aeson
+    , base >=4.7 && <5
+    , base64-bytestring
+    , bytestring
+    , hspec
+    , http-client
+    , http-client-tls
+    , mixpanel-client
+    , servant >=0.14.1
+    , servant-client
+    , string-conv
+    , text
+    , time
+  default-language: Haskell2010
+  build-tool-depends: hspec-discover:hspec-discover
+
+test-suite readme
+  type: exitcode-stdio-1.0
+  main-is: README.lhs
+  other-modules:
+      Paths_mixpanel_client
+  default-extensions: OverloadedStrings LambdaCase DeriveGeneric DeriveFunctor DeriveAnyClass RecordWildCards
+  ghc-options: -Wall -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures -fwarn-name-shadowing -fwarn-incomplete-patterns -pgmL markdown-unlit
+  build-depends:
+      aeson
+    , base
+    , base64-bytestring
+    , bytestring
+    , http-client
+    , http-client-tls
+    , mixpanel-client
+    , servant >=0.14.1
+    , servant-client
+    , string-conv
+    , text
+    , time
+  default-language: Haskell2010
+  build-tool-depends: markdown-unlit:markdown-unlit
diff --git a/src/MixPanel.hs b/src/MixPanel.hs
new file mode 100644
--- /dev/null
+++ b/src/MixPanel.hs
@@ -0,0 +1,91 @@
+module MixPanel
+  ( MixPanelError(..)
+  , AuthToken(..)
+  , Operation(..)
+  , DistinctId
+  -- Setup
+  , mkEnv
+  , Env
+  -- mixpanel api calls
+  , track
+  , alias
+  , engage
+  -- HTTP manager reexports
+  , tlsManagerSettings
+  , HTTP.newManager
+  ) where
+
+import           Data.Aeson                     ( Object, (.=) )
+import           Data.Text                      ( Text )
+import           GHC.Exts                       ( fromList )
+import qualified Network.HTTP.Client           as HTTP
+import           Network.HTTP.Client.TLS        ( tlsManagerSettings )
+import           Servant.API             hiding ( URI )
+import           Servant.Client
+
+import           MixPanel.API                   ( api )
+import           MixPanel.Types.Core            ( DidSucceed(..)
+                                                , Toggle(..)
+                                                , AuthToken(..)
+                                                )
+import           MixPanel.Types.TrackData       ( TrackData(..)
+                                                , mkProperties
+                                                )
+import           MixPanel.Types.EngageData      ( EngageData, DistinctId, Operation(..), mkEngageData )
+
+
+host :: BaseUrl
+host = BaseUrl Https "api.mixpanel.com" 443 ""
+
+data MixPanelError
+  = ServantError ServantError
+  | Error Text
+
+data Env = Env
+  { authtoken :: AuthToken
+  , httpManager :: HTTP.Manager
+  , clientEnv :: ClientEnv
+  }
+
+mkEnv :: AuthToken -> HTTP.Manager -> Env
+mkEnv authtoken httpManager = Env {..}
+  where
+    clientEnv = mkClientEnv httpManager host
+
+
+
+trackC :: TrackData -> Maybe Toggle -> Maybe Text -> Maybe Toggle -> Maybe Text -> Maybe Toggle -> ClientM DidSucceed
+engageC :: EngageData -> Maybe Text -> Maybe Text -> Maybe Toggle -> ClientM DidSucceed
+trackC :<|> engageC = client api
+
+trackC' :: TrackData -> ClientM DidSucceed
+trackC' trackdata = trackC trackdata Nothing Nothing Nothing Nothing (Just On)
+
+engageC' :: EngageData -> ClientM DidSucceed
+engageC' engagedata =
+  engageC engagedata Nothing Nothing (Just On)
+
+runMixPanel :: ClientEnv -> ClientM DidSucceed -> IO (Either MixPanelError ())
+runMixPanel clientEnv comp = do
+  result <- runClientM comp clientEnv
+  return $ case result of
+    Left err -> Left $ ServantError err
+    Right (Fail err) -> Left $ Error err
+    Right Success -> Right ()
+
+track :: Env -> Text -> Object -> IO (Either MixPanelError ())
+track Env{..} event props =
+  runMixPanel clientEnv $ trackC' $ TrackData event $ mkProperties authtoken props
+
+-- | Renames distinct id into alias id
+alias :: Env -> DistinctId -> Text -> IO (Either MixPanelError ())
+alias Env{..} distinctId aliasId =
+   runMixPanel clientEnv $ trackC' $ TrackData "$create_alias" $ mkProperties authtoken props
+    where
+      props :: Object
+      props = fromList [ "alias" .= aliasId
+                       , "distinct_id" .= distinctId]
+
+engage :: Env -> DistinctId -> Operation -> IO (Either MixPanelError ())
+engage Env{..} distinctid operation =
+  runMixPanel clientEnv $ engageC' $ mkEngageData authtoken distinctid operation
diff --git a/src/MixPanel/API.hs b/src/MixPanel/API.hs
new file mode 100644
--- /dev/null
+++ b/src/MixPanel/API.hs
@@ -0,0 +1,38 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeOperators #-}
+{- As per https://mixpanel.com/help/reference/http
+-}
+module MixPanel.API
+  ( API
+  , api
+  ) where
+
+import           Data.Text                      ( Text )
+import           Data.Proxy                     ( Proxy(..) )
+import           Servant.API             hiding ( URI )
+
+import           MixPanel.Types.Core            ( DidSucceed, Toggle )
+import           MixPanel.Types.TrackData       ( TrackData )
+import           MixPanel.Types.EngageData      ( EngageData )
+
+
+type Track = "track"
+          :> QueryParam' '[Required] "data" TrackData
+          :> QueryParam "ip" Toggle
+          :> QueryParam "redirect" Text -- URI
+          :> QueryParam "img" Toggle
+          :> QueryParam "callback" Text
+          :> QueryParam "verbose" Toggle
+          :> Get '[JSON] DidSucceed
+
+type Engage = "engage"
+           :> QueryParam' '[Required] "data" EngageData
+           :> QueryParam "redirect" Text -- URI
+           :> QueryParam "callback" Text
+           :> QueryParam "verbose" Toggle
+           :> Get '[JSON] DidSucceed
+
+type API = Track :<|> Engage
+
+api :: Proxy API
+api = Proxy
diff --git a/src/MixPanel/Types/Core.hs b/src/MixPanel/Types/Core.hs
new file mode 100644
--- /dev/null
+++ b/src/MixPanel/Types/Core.hs
@@ -0,0 +1,44 @@
+module MixPanel.Types.Core
+  ( AuthToken(..)
+  , Toggle(..)
+  , DidSucceed(..)
+  ) where
+
+import           Data.Aeson                     ( ToJSON
+                                                , FromJSON
+                                                , parseJSON
+                                                , toJSON
+                                                , withObject
+                                                , (.:)
+                                                )
+import           Data.Text                      ( Text )
+import           GHC.Generics                   ( Generic )
+import           Servant.API
+
+newtype AuthToken = AuthToken Text
+  deriving (Generic, ToJSON)
+
+-- | MixPanel API uses boolean logic with 1 or 0,
+-- | so we have a special type for it
+data Toggle = On | Off
+  deriving (Show)
+
+instance ToJSON Toggle where
+  toJSON On = "1"
+  toJSON Off = "0"
+
+instance ToHttpApiData Toggle where
+  toUrlPiece On = "1"
+  toUrlPiece Off = "0"
+
+
+data DidSucceed = Success | Fail Text
+  deriving (Show)
+
+instance FromJSON DidSucceed where
+  parseJSON = withObject "success or failure" $ \o -> do
+    status <- o .: "status"
+    case (status :: Int) of
+      1 -> return Success
+      0 -> Fail <$> o .: "error"
+      _ -> fail ("unknown status: " ++ show status)
diff --git a/src/MixPanel/Types/EngageData.hs b/src/MixPanel/Types/EngageData.hs
new file mode 100644
--- /dev/null
+++ b/src/MixPanel/Types/EngageData.hs
@@ -0,0 +1,89 @@
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE FlexibleContexts #-}
+module MixPanel.Types.EngageData
+  ( EngageData
+  , DistinctId
+  , mkEngageData
+  , Operation(..)
+  ) where
+
+import           Data.Text                      ( Text )
+import           Data.Aeson                     ( ToJSON
+                                                , toJSON
+                                                , Object
+                                                , Value(..)
+                                                , Array
+                                                , encode
+                                                , (.=)
+                                                )
+import qualified Data.ByteString.Base64.Lazy   as B64
+import           Data.String.Conv               ( toS )
+import           GHC.Exts                       ( fromList )
+import           Servant.API
+
+import           MixPanel.Types.Core            ( AuthToken )
+
+
+data Operation where
+  Set :: Object -> Operation
+  SetOnce :: Object -> Operation
+  Add :: Object -> Operation
+  Append :: Object -> Operation
+  Union :: Object -> Operation
+  Remove :: Object -> Operation
+  Unset :: Array -> Operation
+  Delete :: Operation
+
+instance ToJSON Operation where
+  toJSON (Set obj) = Object obj
+  toJSON (SetOnce obj) = Object obj
+  toJSON (Add obj) = Object obj
+  toJSON (Append obj) = Object obj
+  toJSON (Union obj) = Object obj
+  toJSON (Remove obj) = Object obj
+  toJSON (Unset obj) = Array obj
+  toJSON Delete = ""
+
+operationIdentifier :: Operation -> Text
+operationIdentifier (Set _) = "$set"
+operationIdentifier (SetOnce _) = "$set_once"
+operationIdentifier (Add _) = "$add"
+operationIdentifier (Append _) = "$append"
+operationIdentifier (Union _) = "$union"
+operationIdentifier (Remove _) = "$remove"
+operationIdentifier (Unset _) = "$unset"
+operationIdentifier Delete = "$delete"
+
+type DistinctId = Text
+
+data EngageData = EngageData
+  { token :: AuthToken
+  , distinctId :: DistinctId
+  , ip :: Maybe Text
+  , time :: Maybe Text
+  , ignoreTime :: Maybe Text
+  , operation :: Operation
+  }
+
+instance ToJSON EngageData where
+  toJSON EngageData{..} = Object $ fromList
+    [ "$token" .= token
+    , "$distinct_id" .= distinctId
+    , "$time" .= time
+    , "$ignoreTime" .= ignoreTime
+    , "$ip" .= ip
+    , operationIdentifier operation .= toJSON operation
+    ]
+
+instance ToHttpApiData EngageData where
+  toUrlPiece = toS . B64.encode . encode
+
+mkEngageData :: AuthToken -> DistinctId -> Operation -> EngageData
+mkEngageData token di op = EngageData
+  { token = token
+  , distinctId = di
+  , ip = Nothing
+  , time = Nothing
+  , ignoreTime = Nothing
+  , operation = op
+  }
diff --git a/src/MixPanel/Types/TrackData.hs b/src/MixPanel/Types/TrackData.hs
new file mode 100644
--- /dev/null
+++ b/src/MixPanel/Types/TrackData.hs
@@ -0,0 +1,57 @@
+{-# LANGUAGE ExistentialQuantification #-}
+module MixPanel.Types.TrackData
+  ( TrackData(..)
+  , Properties
+  , mkProperties
+  ) where
+
+import           Data.Aeson                     ( ToJSON
+                                                , toJSON
+                                                , encode
+                                                , Value(..)
+                                                , Object
+                                                , (.=)
+                                                )
+import           Data.Text                      ( Text )
+import qualified Data.ByteString.Base64.Lazy   as B64
+import           Data.Time.Clock.POSIX          ( POSIXTime )
+import           GHC.Exts                       ( fromList )
+import           GHC.Generics                   ( Generic )
+import           Servant.API
+import           Data.String.Conv               ( toS )
+import           MixPanel.Types.Core            ( AuthToken )
+
+
+data TrackData = TrackData
+  { event :: Text
+  , properties :: Properties
+  } deriving (Generic, ToJSON)
+
+
+instance ToHttpApiData TrackData where
+  toUrlPiece = toS . B64.encode . encode
+
+data Properties = Properties
+  { token :: AuthToken
+  , distinctId :: Maybe Text
+  , time :: Maybe POSIXTime
+  , ip :: Maybe Text
+  , extraProperties :: Object
+  }
+
+instance ToJSON Properties where
+  toJSON Properties{..} = Object $ extraProperties <> fromList
+     [ "token" .= token
+     , "distinct_id" .= distinctId
+     , "time" .= time
+     , "ip" .= ip
+     ]
+
+mkProperties :: AuthToken -> Object -> Properties
+mkProperties token obj = Properties
+  { token = token
+  , distinctId = Nothing
+  , time = Nothing
+  , ip = Nothing
+  , extraProperties = obj
+  }
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,11 @@
+module Main where
+
+import Test.Hspec.Runner
+import qualified Spec
+
+main :: IO ()
+main = hspecWith config Spec.spec
+  where
+    config = defaultConfig
+      { configColorMode = ColorAlways
+      }
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 -optF --module-name=Spec #-}
diff --git a/test/TypesSpec.hs b/test/TypesSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/TypesSpec.hs
@@ -0,0 +1,23 @@
+module TypesSpec where
+
+import Data.Aeson
+import GHC.Exts   (fromList)
+import Test.Hspec
+
+import MixPanel.Types.Core
+import MixPanel.Types.EngageData
+import MixPanel.Types.TrackData
+
+
+spec :: Spec
+spec = describe "TrackData/EngageData" $ do
+  it "TrackData merges properties" $
+    let
+     td = TrackData "foobar" $ mkProperties (AuthToken "token123") props
+     props :: Object
+     props = fromList [ "distinct_id" .= ("bar" :: String) ]
+    in encode td `shouldBe` "{\"event\":\"foobar\",\"properties\":{\"time\":null,\"token\":\"token123\",\"ip\":null,\"distinct_id\":\"bar\"}}"
+  it "EngageData merges properties" $
+    let
+      ed = mkEngageData (AuthToken "foobar") "123" $ Set (fromList [ "customProperty" .= ("foobar" :: String)])
+    in encode ed `shouldBe` "{\"$distinct_id\":\"123\",\"$set\":{\"customProperty\":\"foobar\"},\"$token\":\"foobar\",\"$ip\":null,\"$ignoreTime\":null,\"$time\":null}"
