diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Sean Hess (c) 2017
+
+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 Sean Hess 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/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/datarobot.cabal b/datarobot.cabal
new file mode 100644
--- /dev/null
+++ b/datarobot.cabal
@@ -0,0 +1,72 @@
+-- This file has been generated from package.yaml by hpack version 0.17.0.
+--
+-- see: https://github.com/sol/hpack
+
+name:                datarobot
+version:             0.1.0
+synopsis:            Client for DataRobot API
+description:         Client for DataRobot API
+homepage:            https://github.com/orbital/datarobot#readme
+bug-reports:         https://github.com/orbital/datarobot/issues
+license:             BSD3
+license-file:        LICENSE
+author:              Sean Hess
+maintainer:          seanhess@gmail.com
+copyright:           Orbital Labs
+category:            Web
+build-type:          Simple
+cabal-version:       >= 1.10
+
+source-repository head
+  type: git
+  location: https://github.com/orbital/datarobot
+
+library
+  hs-source-dirs:
+      src
+  exposed-modules:
+      DataRobot
+      DataRobot.API
+      DataRobot.Features
+      DataRobot.Predict
+      DataRobot.PredictResponse
+      DataRobot.Types
+  build-depends:
+      base >=4.7 && <5
+    , aeson
+    , bytestring
+    , exceptions
+    , microlens
+    , network-uri
+    , safe
+    , scientific
+    , string-conversions
+    , text
+    , unordered-containers
+    , vector
+    , wreq
+  default-language: Haskell2010
+
+test-suite test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      base >=4.7 && <5
+    , aeson
+    , bytestring
+    , exceptions
+    , microlens
+    , network-uri
+    , safe
+    , scientific
+    , string-conversions
+    , text
+    , unordered-containers
+    , vector
+    , wreq
+    , base
+    , datarobot
+  default-language: Haskell2010
diff --git a/src/DataRobot.hs b/src/DataRobot.hs
new file mode 100644
--- /dev/null
+++ b/src/DataRobot.hs
@@ -0,0 +1,42 @@
+module DataRobot
+  (
+  -- * Types
+    Credentials(..)
+  , defaultBaseURL
+  , defaultBaseURLPredict
+  , ProjectID(..)
+  , ModelID(..)
+
+  -- * Features API
+  , features
+  , Features(..)
+
+  -- * Predict API
+  , predict
+  , Fields
+  , PredictError(..)
+  , PredictResult(..)
+  , responseResult
+  , classProbability
+
+
+
+  ) where
+
+import DataRobot.Features
+import DataRobot.Predict
+import DataRobot.PredictResponse
+import DataRobot.Types
+import Network.URI (URI, parseURI)
+
+
+defaultBaseURL :: URI
+defaultBaseURL =
+    let (Just u) = parseURI "https://app.datarobot.com/api/v2/"
+    in u
+
+
+defaultBaseURLPredict :: URI
+defaultBaseURLPredict =
+    let (Just u) = parseURI "https://simplefinance.orm.datarobot.com/api/v1/"
+    in u
diff --git a/src/DataRobot/API.hs b/src/DataRobot/API.hs
new file mode 100644
--- /dev/null
+++ b/src/DataRobot/API.hs
@@ -0,0 +1,29 @@
+module DataRobot.API where
+
+import Lens.Micro ((^.))
+import Data.ByteString.Lazy (ByteString)
+import Data.Aeson (FromJSON, eitherDecode)
+import Data.List (intercalate)
+import Data.Typeable (Typeable)
+import Data.Monoid ((<>))
+import Control.Monad.Catch (MonadThrow, Exception, throwM)
+import Network.URI (URI(..), pathSegments, uriToString)
+import Network.Wreq (Response, responseBody)
+
+data JSONError = JSONError String ByteString
+               deriving (Typeable, Show)
+instance Exception JSONError
+
+
+parseResponse :: (FromJSON a, MonadThrow m) => Response ByteString -> m a
+parseResponse r =
+  case eitherDecode $ r ^. responseBody of
+    Left err -> throwM $ JSONError err (r ^. responseBody)
+    Right p -> pure p
+
+
+endpoint :: URI -> [String] -> String
+endpoint base ps =
+    let ps' = "" : pathSegments base <> ps
+        u = base { uriPath = intercalate "/" ps' }
+    in uriToString id u ""
diff --git a/src/DataRobot/Features.hs b/src/DataRobot/Features.hs
new file mode 100644
--- /dev/null
+++ b/src/DataRobot/Features.hs
@@ -0,0 +1,51 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings #-}
+module DataRobot.Features where
+
+import Lens.Micro ((.~))
+import Control.Monad.IO.Class (MonadIO, liftIO)
+import Control.Monad.Catch (MonadThrow)
+import Data.Monoid ((<>))
+import Data.ByteString (ByteString)
+import Data.Function ((&))
+import Data.String.Conversions (cs)
+
+import DataRobot.Types
+import DataRobot.API (parseResponse, endpoint)
+
+import Network.URI (URI(..))
+import qualified Network.Wreq as Wreq
+import Network.Wreq (defaults, header, checkResponse)
+import Network.Wreq.Types (ResponseChecker)
+
+
+
+-- GET https://app.datarobot.com/api/v2/projects/5988c39bc808917519a2acbb/models/5988d164c8089128924bd6cf/features
+features :: (MonadIO m, MonadThrow m) => Credentials -> ProjectID -> ModelID -> m Features
+features c pid mid = do
+    let opts = httpOptions c
+        url = featuresEndpoint (baseURL c) pid mid
+    r <- liftIO $ Wreq.getWith opts url
+    f <- parseResponse r
+    pure f
+
+
+featuresEndpoint :: URI -> ProjectID -> ModelID -> String
+featuresEndpoint base (ProjectID pid) (ModelID mid) =
+    endpoint base ["projects", cs pid, "models", cs mid, "features"]
+
+
+
+
+httpOptions :: Credentials -> Wreq.Options
+httpOptions c =
+    defaults
+      & header "Authorization" .~ [authorization]
+      & header "datarobot-key" .~ [apiKey c]
+      & checkResponse .~ Just ignoreStatus
+  where
+    ignoreStatus :: ResponseChecker
+    ignoreStatus _ _ = pure ()
+
+    authorization :: ByteString
+    authorization = "Token " <> (apiToken c)
diff --git a/src/DataRobot/Predict.hs b/src/DataRobot/Predict.hs
new file mode 100644
--- /dev/null
+++ b/src/DataRobot/Predict.hs
@@ -0,0 +1,63 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings #-}
+module DataRobot.Predict
+  ( predict
+  , predictURI
+  , Credentials(..)
+  , ProjectID(..)
+  , ModelID(..)
+  ) where
+
+import Lens.Micro ((?~), (.~))
+import Control.Monad.IO.Class (MonadIO, liftIO)
+import Control.Monad.Catch (MonadThrow)
+import Data.Aeson (Value(..))
+import Data.Function ((&))
+import qualified Data.HashMap.Strict as HM
+import qualified Data.Vector as V
+import Data.String.Conversions (cs)
+
+import DataRobot.Types
+import DataRobot.API (parseResponse, endpoint)
+import DataRobot.PredictResponse
+
+import Network.URI (URI(..))
+import qualified Network.Wreq as Wreq
+import Network.Wreq (defaults, Auth, basicAuth, auth, header, checkResponse)
+import Network.Wreq.Types (ResponseChecker)
+
+
+-- https://app.datarobot.com/docs/users-guide/basics/predictions/prediction-api.html
+predict :: (MonadIO m, MonadThrow m) => Credentials -> ProjectID -> ModelID -> Fields -> m (Either PredictError PredictResult)
+predict c pid mid o = do
+  let opts = httpOptions c
+      url = predictURI (baseURLPredict c) pid mid
+      body = Array $ V.singleton $ Object $ HM.fromList o
+  r <- liftIO $ Wreq.postWith opts url body
+  pr <- parseResponse r
+  pure $ responseResult pr
+
+
+httpOptions :: Credentials -> Wreq.Options
+httpOptions c =
+    defaults
+      & auth ?~ (authorization c)
+      & header "datarobot-key" .~ [apiKey c]
+      & checkResponse .~ Just ignoreStatus
+  where
+    ignoreStatus :: ResponseChecker
+    ignoreStatus _ _ = pure ()
+
+
+
+predictURI :: URI -> ProjectID -> ModelID -> String
+predictURI base (ProjectID pid) (ModelID mid) =
+    endpoint base [cs pid, cs mid, "predict"]
+
+
+
+authorization :: Credentials -> Auth
+authorization c =
+  basicAuth (username c) (apiToken c)
+
+
diff --git a/src/DataRobot/PredictResponse.hs b/src/DataRobot/PredictResponse.hs
new file mode 100644
--- /dev/null
+++ b/src/DataRobot/PredictResponse.hs
@@ -0,0 +1,94 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+module DataRobot.PredictResponse
+  ( PredictError(..)
+  , PredictResult(..)
+  , responseResult
+  , classProbability
+  ) where
+
+import Control.Applicative ((<|>))
+import Control.Monad.Catch (Exception)
+import Data.Aeson (FromJSON(..), ToJSON, Value)
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as HM
+import Data.Maybe (fromMaybe)
+import Data.Text (Text)
+import Data.Typeable (Typeable)
+import GHC.Generics (Generic)
+import Safe (headMay)
+
+
+data PredictError
+    = APIError Code Text
+    | MissingPrediction
+    deriving (Typeable, Show)
+
+instance Exception PredictError
+
+
+newtype PredictResponse = PredictResponse (Either PredictFailure PredictSuccess )
+    deriving (Show, Eq)
+
+instance FromJSON PredictResponse where
+    parseJSON v = PredictResponse <$>
+      ((Right <$> parseJSON v) <|> (Left <$> parseJSON v))
+
+type Code = Int
+
+data PredictFailure = PredictFailure
+  { code :: Code
+  , status :: Text
+  } deriving (Show, Eq, Generic)
+
+instance FromJSON PredictFailure
+
+data PredictSuccess = PredictSuccess
+  { predictions :: [Prediction]
+  , execution_time :: Float
+  , model_id :: Text
+  -- , task :: Text
+  } deriving (Show, Eq, Generic)
+
+instance FromJSON PredictSuccess
+
+data Prediction = Prediction
+  { prediction :: Value
+  , class_probabilities :: Maybe (HashMap Text Float)
+  } deriving (Show, Eq, Generic)
+
+instance FromJSON Prediction
+
+
+
+-- | Result from the prediction
+
+data PredictResult = PredictResult
+  { prediction :: Value
+  , predictionTimeMs :: Float
+  , modelId :: Text
+  , classProbabilities :: Maybe (HashMap Text Float)
+  } deriving (Show, Eq, Generic)
+
+instance ToJSON PredictResult
+
+responseResult :: PredictResponse -> Either PredictError PredictResult
+responseResult (PredictResponse (Right ps)) =
+      fromMaybe (Left MissingPrediction) $ do
+        p <- headMay (predictions ps)
+        return $ Right $ PredictResult
+          { prediction = prediction (p :: Prediction)
+          , predictionTimeMs = execution_time ps
+          , modelId = model_id ps
+          , classProbabilities = class_probabilities p
+          }
+responseResult (PredictResponse (Left pf)) =
+    Left $ APIError (code pf) (status pf)
+
+
+classProbability :: Text -> PredictResult -> Maybe Float
+classProbability c r = do
+    cps <- classProbabilities r
+    HM.lookup c cps
+
diff --git a/src/DataRobot/Types.hs b/src/DataRobot/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/DataRobot/Types.hs
@@ -0,0 +1,37 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE DeriveGeneric #-}
+module DataRobot.Types where
+
+
+import Data.ByteString (ByteString)
+import Data.Aeson (ToJSON(..), FromJSON(..), Value)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+import Network.URI (URI(..))
+
+data Credentials = Credentials
+    { apiToken :: ByteString
+    , apiKey :: ByteString
+    , username :: ByteString
+    , baseURLPredict :: URI
+    , baseURL :: URI
+    } deriving (Show, Eq)
+
+
+newtype ProjectID = ProjectID Text
+  deriving (Show, Eq, ToJSON, FromJSON)
+
+
+newtype ModelID = ModelID Text
+  deriving (Show, Eq, ToJSON, FromJSON)
+
+
+data Features = Features
+    { featureNames :: [Text]
+    } deriving (Show, Eq, Generic)
+instance ToJSON Features
+instance FromJSON Features
+
+
+type Fields = [(Text, Value)]
+
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,2 @@
+main :: IO ()
+main = putStrLn "Test suite not yet implemented"
