diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2017, Brian McKenna
+
+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 Brian McKenna 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/Network/Roku.hs b/Network/Roku.hs
new file mode 100644
--- /dev/null
+++ b/Network/Roku.hs
@@ -0,0 +1,10 @@
+module Network.Roku
+  ( module Network.Roku.App
+  , module Network.Roku.Keys
+  , module Network.Roku.Request
+  )
+where
+
+import Network.Roku.App
+import Network.Roku.Keys
+import Network.Roku.Request
diff --git a/Network/Roku/App.hs b/Network/Roku/App.hs
new file mode 100644
--- /dev/null
+++ b/Network/Roku/App.hs
@@ -0,0 +1,62 @@
+module Network.Roku.App where
+
+import Control.Applicative
+import Data.Text
+import Text.XML.Light.Extractors
+
+newtype AppId
+  = AppId Text
+  deriving Show
+
+data AppType
+  = Appl
+  | Ssvr
+  deriving Show
+
+newtype AppVersion
+  = AppVersion Text
+  deriving Show
+
+newtype AppName
+  = AppName Text
+  deriving Show
+
+data App
+  = App AppId AppType AppVersion AppName
+  deriving Show
+
+data AppOrHomescreen
+  = NormalApp App
+  | Homescreen
+  deriving Show
+
+data Screensaver
+  = Screensaver App
+  deriving Show
+
+data ActiveAppResponse
+  = ActiveAppResponse AppOrHomescreen (Maybe Screensaver)
+  deriving Show
+
+appBodyParser :: ElementExtractor App
+appBodyParser = do
+  let p = Right . pack
+  i <- attribAs "id" p
+  let t' "appl" = Right Appl
+      t' "ssvr" = Right Ssvr
+      t' t = Left t
+  t <- attribAs "type" t'
+  v <- attribAs "version" p
+  n <- contents $ textAs p
+  return $ App (AppId i) t (AppVersion v) (AppName n)
+
+appsParser :: ContentsExtractor [App]
+appsParser =
+  element "apps" . children . many $ element "app" appBodyParser
+
+activeAppParser :: ContentsExtractor ActiveAppResponse
+activeAppParser =
+  element "active-app" . children $ do
+    app <- optional $ element "app" appBodyParser
+    ssvr <- optional $ element "screensaver" appBodyParser
+    return $ ActiveAppResponse (maybe Homescreen NormalApp app) $ Screensaver <$> ssvr
diff --git a/Network/Roku/Keys.hs b/Network/Roku/Keys.hs
new file mode 100644
--- /dev/null
+++ b/Network/Roku/Keys.hs
@@ -0,0 +1,58 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Network.Roku.Keys where
+
+import Prelude hiding (Either(..))
+import Data.ByteString.Char8
+
+data Key
+  = Home
+  | Rev
+  | Fwd
+  | Play
+  | Select
+  | Left
+  | Right
+  | Down
+  | Up
+  | Back
+  | InstantReplay
+  | Info
+  | Backspace
+  | Search
+  | Enter
+  | Lit Char
+  deriving Show
+
+keyValue :: Key -> ByteString
+keyValue Home =
+  "Home"
+keyValue Rev =
+  "Rev"
+keyValue Fwd =
+  "Fwd"
+keyValue Play =
+  "Play"
+keyValue Select =
+  "Select"
+keyValue Left =
+  "Left"
+keyValue Right =
+  "Right"
+keyValue Down =
+  "Down"
+keyValue Up =
+  "Up"
+keyValue Back =
+  "Back"
+keyValue InstantReplay =
+  "InstantReplay"
+keyValue Info =
+  "Info"
+keyValue Backspace =
+  "Backspace"
+keyValue Search =
+  "Search"
+keyValue Enter =
+  "Enter"
+keyValue (Lit c) =
+  snoc "Lit_" c
diff --git a/Network/Roku/Request.hs b/Network/Roku/Request.hs
new file mode 100644
--- /dev/null
+++ b/Network/Roku/Request.hs
@@ -0,0 +1,99 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Network.Roku.Request
+  ( Query(..)
+  , Method(..)
+  , Request(..)
+  , Resource(..)
+  , request
+  , resource
+  , QueryError(..)
+  , queryActiveApp
+  , queryApps
+  )
+where
+
+import Data.ByteString
+import Data.Semigroup
+import Data.Text.Encoding
+import Data.Bifunctor
+import Network.Roku.App
+import Text.XML.Light.Extractors
+import Text.XML.Light.Input
+import qualified Data.ByteString.Lazy as LBS
+import qualified Network.HTTP.Client as HTTP
+import qualified Network.Roku.Keys as Keys
+
+data Query
+  = Apps
+  | ActiveApp
+  | Icon AppId
+  deriving Show
+
+data Method
+  = Get
+  | Post
+  deriving Show
+
+data Request
+  = Query Query
+  | Keyup Keys.Key
+  | Keydown Keys.Key
+  | Keypress Keys.Key
+  | Launch AppId
+  | Install AppId
+  deriving Show
+
+data Resource
+  = Resource Method ByteString
+  deriving Show
+
+resource :: Request -> Resource
+resource (Query query) =
+  Resource Get $ "query/" <> path
+  where path = case query of
+          Apps -> "apps"
+          ActiveApp -> "active-app"
+          Icon (AppId appId) -> "icon/" <> encodeUtf8 appId
+resource (Keyup key) =
+  Resource Post $ "keyup/" <> Keys.keyValue key
+resource (Keydown key) =
+  Resource Post $ "keydown/" <> Keys.keyValue key
+resource (Keypress key) =
+  Resource Post $ "keypress/" <> Keys.keyValue key
+resource (Launch (AppId appId)) =
+  Resource Post $ "launch/" <> encodeUtf8 appId
+resource (Install (AppId appId)) =
+  Resource Post $ "install/" <> encodeUtf8 appId
+
+data QueryError
+  = QueryInvalidXML LBS.ByteString
+  | QueryExtractionErr ExtractionErr
+  deriving Show
+
+request :: Request -> ByteString -> HTTP.Request
+request r host =
+  HTTP.defaultRequest { HTTP.host = host
+                      , HTTP.method = method'
+                      , HTTP.path = path
+                      , HTTP.port = 8060
+                      }
+  where Resource method path = resource r
+        method' = case method of
+          Post -> "POST"
+          Get -> "GET"
+
+runQuery :: Query -> ContentsExtractor a -> ByteString -> HTTP.Manager -> IO (Either QueryError a)
+runQuery q p host manager = do
+  r <- HTTP.httpLbs (request (Query q) host) manager
+  let body = HTTP.responseBody r
+  return $ do
+    doc <- maybe (Left $ QueryInvalidXML body) Right $ parseXMLDoc body
+    first QueryExtractionErr $ extractDocContents p doc
+
+queryActiveApp :: ByteString -> HTTP.Manager -> IO (Either QueryError ActiveAppResponse)
+queryActiveApp = do
+  runQuery ActiveApp activeAppParser
+
+queryApps :: ByteString -> HTTP.Manager -> IO (Either QueryError [App])
+queryApps = do
+  runQuery Apps appsParser
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/roku-api.cabal b/roku-api.cabal
new file mode 100644
--- /dev/null
+++ b/roku-api.cabal
@@ -0,0 +1,29 @@
+name:                roku-api
+version:             0.1.0.0
+synopsis:            Bindings to Roku's External Control API
+description:         Bindings to Roku's External Control API.
+license:             BSD3
+license-file:        LICENSE
+author:              Brian McKenna
+maintainer:          brian@brianmckenna.org
+category:            Network
+build-type:          Simple
+cabal-version:       >=1.10
+
+library
+  exposed-modules:     Network.Roku
+                     , Network.Roku.App
+                     , Network.Roku.Keys
+                     , Network.Roku.Request
+  build-depends:       base >=4.9 && <4.10
+                     , bytestring
+                     , http-client
+                     , network
+                     , text
+                     , xml
+                     , xml-extractors
+  default-language:    Haskell2010
+
+source-repository head
+  type:     git
+  location: https://github.com/puffnfresh/roku-api
