diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -1,6 +1,6 @@
 module Main where
 
-import Web.Exec
+import Web.Tweet.Exec
 
 main :: IO ()
 main = exec
diff --git a/clit.cabal b/clit.cabal
--- a/clit.cabal
+++ b/clit.cabal
@@ -1,5 +1,5 @@
 name:                clit
-version:             0.1.1.2
+version:             0.2.0.0
 synopsis:            Post tweets from stdin
 description:         a Command Line Interface Tweeter
 homepage:            https://github.com/vmchale/command-line-tweeter#readme
@@ -16,7 +16,10 @@
 library
   hs-source-dirs:      src
   exposed-modules:     Web.Tweet
-                     , Web.Exec
+                     , Web.Tweet.Exec
+  other-modules:       Web.Tweet.Types
+                     , Web.Tweet.Utils
+                     , Web.Tweet.Sign
   build-depends:       base >= 4.7 && < 5
                      , aeson
                      , http-client-tls
@@ -26,6 +29,8 @@
                      , bytestring
                      , split
                      , optparse-applicative < 0.13.0.0
+                     , lens
+                     , data-default
   default-language:    Haskell2010
 
 executable tweet
diff --git a/src/Web/Exec.hs b/src/Web/Exec.hs
deleted file mode 100644
--- a/src/Web/Exec.hs
+++ /dev/null
@@ -1,46 +0,0 @@
--- | Provides IO action that parses command line options and tweets from stdin
-module Web.Exec (exec) where
-
-import Web.Tweet
-import Options.Applicative
-import qualified Data.ByteString.Char8 as BS
-import Data.List.Split (chunksOf)
-
--- | Executes parser
-exec :: IO ()
-exec = execParser opts >>= select
-    where 
-        opts = info (helper <*> program)
-            (fullDesc
-            <> progDesc "Tweet from stdin!"
-            <> header "clit - a Command Line Interface Tweeter")
-
--- | query twitter to post stdin
-fromStdIn :: Int -> FilePath -> IO ()
-fromStdIn num filepath = do
-    content <- fmap ((take num) . (map BS.pack) .  (chunksOf 140)) getContents
-    sequence_ $ fmap (tweet filepath) content
-
--- | Data type for our program; just one optional argument for path to credential file.
-data Program = Program { cred :: Maybe FilePath , tweets :: Maybe String }
-
--- | Executes program
-select :: Program -> IO ()
-select (Program Nothing (Just n)) = fromStdIn (read n) ".cred"
-select (Program Nothing Nothing) = fromStdIn 4 ".cred"
-select (Program (Just file) (Just n)) = fromStdIn (read n) file
-select (Program (Just file) Nothing) = fromStdIn 4 file
-
--- | Parser to return a program datatype
-program :: Parser Program
-program = Program
-    <$> (optional $ strOption
-        (long "cred"
-        <> short 'c'
-        <> metavar "CREDENTIALS"
-        <> help "path to credentials"))
-    <*> (optional $ strOption
-        (long "tweets"
-        <> short 't'
-        <> metavar "NUM"
-        <> help "Number of tweets in a row, default 4"))
diff --git a/src/Web/Tweet.hs b/src/Web/Tweet.hs
--- a/src/Web/Tweet.hs
+++ b/src/Web/Tweet.hs
@@ -1,10 +1,11 @@
 {-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE DeriveGeneric     #-}
 
 -- | Various utilities to tweet using the twitter api
 -- 
--- Make sure you have a file credentials file (default `.cred`) with the following info:
+-- Make sure you have a file credentials file (default the executable looks for is `.cred`) with the following info:
 --
+-- ```
+--
 -- api-key: API_KEY
 --
 -- api-sec: API_SECRE
@@ -12,88 +13,95 @@
 -- tok: OAUTH_TOKEN
 --
 -- tok-sec: TOKEN_SECRET
+--
+-- ```
 
 module Web.Tweet
-    ( tweet
+    (
+    -- * Functions to tweet
+    basicTweet
+    , tweetData
+    , thread
+    -- * Data type for a tweet
+    , module Web.Tweet.Types
+    -- * Functions to sign API requests
     , signRequest
+    -- * Functions to generate a URL string from a `Tweet`
     , urlString
     ) where
 
-import Data.Aeson
-import GHC.Generics
 import Network.HTTP.Client
 import Network.HTTP.Client.TLS
 import Network.HTTP.Types.Status (statusCode)
-import Web.Authenticate.OAuth
 import qualified Data.ByteString.Char8 as BS
 import qualified Data.ByteString.Lazy.Char8 as BSL
 import Data.Char (toLower)
+import Web.Tweet.Types
+import Web.Tweet.Utils
+import Control.Monad
+import Data.List.Split (chunksOf)
+import Data.Maybe
+import Control.Lens
+import Web.Authenticate.OAuth
+import Web.Tweet.Sign
 
--- | Data type for our request
-data Tweet = Tweet
-    { status    :: String
-    , trim_user :: Bool
-    } deriving Generic
+-- | thread tweets together nicely. Takes a string, a list of handles to reply to, plus the ID of the status you're replying to.
+-- If you need to thread tweets without replying, pass a `Nothing` as the third argument.
+-- `thread "Hi I'm back in New York!" ["friend1","friend2"] Nothing 1 ".cred"`
+thread :: String -> [String] -> Maybe Int -> Int -> FilePath -> IO ()
+thread contents hs idNum num filepath = do
+    let handleStr = concatMap (((++) " ") . ((++) "@")) hs
+    let content = (take num) . (chunksOf (140-(length handleStr))) $ contents
+    print $ urlString (Tweet { _status = content !! 0, _trimUser = True, _handles = hs, _replyID = idNum})
+    let f = (\str i -> (flip tweetData filepath) (Tweet { _status = str, _trimUser = True, _handles = hs, _replyID = if i == 0 then Nothing else Just i }))
+    let initial = f (content !! 0)
+    void $ foldr ((>=>) . f) initial (drop 1 content) $ maybe 0 id idNum
 
-instance ToJSON Tweet where
+-- | Reply with a single tweet. Works the same as `thread` but doesn't take the fourth argument.
+-- `reply "Idk what that means" ["friend1"] (Just 189943500) ".cred"`
+reply :: String -> [String] -> Maybe Int -> FilePath -> IO ()
+reply contents hs idNum filepath = thread contents hs idNum 1 filepath
 
--- | tweet a byteString, with credentials from a given file
-tweet :: FilePath -> BS.ByteString -> IO ()
-tweet filepath content = do
-    requestString <- urlString content
+-- | Tweet a string given a path to credentials; return the id of the status.
+-- `basicTweet "On the airplane." ".cred"
+basicTweet :: String -> FilePath -> IO Int
+basicTweet contents = tweetData (mkTweet contents)
+
+-- | Make a `Tweet` with only the contents.
+mkTweet :: String -> Tweet
+mkTweet contents = over (status) (const (contents)) $ def 
+
+-- | tweet, given a `Tweet` and path to credentials. Return id of posted tweet.
+tweetData :: Tweet -> FilePath -> IO Int
+tweetData tweet filepath = do
+    let requestString = urlString tweet
     manager <- newManager tlsManagerSettings
     initialRequest <- parseRequest ("https://api.twitter.com/1.1/statuses/update.json" ++ requestString)
     request <- signRequest filepath $ initialRequest { method = "POST" }
     response request manager
 
--- | print output of a request
-response :: Request -> Manager -> IO ()
+-- | print output of a request and return status id as an `Int`. 
+response :: Request -> Manager -> IO Int
 response request manager = do
     response <- httpLbs request manager
     putStrLn $ "The status code was: " ++ show (statusCode $ responseStatus response)
     BSL.putStrLn $ responseBody response
-
--- | Sign a request using your OAuth dev token.
--- Uses the IO monad because signatures require a timestamp
-signRequest :: FilePath -> Request -> IO Request
-signRequest filepath req = do
-    o <- oAuth filepath
-    c <- credential filepath
-    signOAuth o c req
-
--- | Create an OAuth token
-oAuth :: FilePath -> IO OAuth
-oAuth filepath = do
-    secret <- (lineByKey "api-sec") <$> getConfigData filepath
-    key <- (lineByKey "api-key") <$> getConfigData filepath
-    let url = "api.twitter.com"
-    return newOAuth { oauthConsumerKey = key , oauthConsumerSecret = secret , oauthServerName = url }
-
--- | Create a new credential from a token and secret component of that token
-credential :: FilePath -> IO Credential
-credential filepath = newCredential <$> token <*> secretToken
-    where token       = (lineByKey "tok") <$> getConfigData filepath
-          secretToken = (lineByKey "tok-sec") <$> getConfigData filepath
-
--- | Pick out a key value from a key
-lineByKey :: BS.ByteString -> [(BS.ByteString, BS.ByteString)] -> BS.ByteString
-lineByKey key = snd . head . (filter (\i -> fst i == key))
-
--- | Get pairs of "key" to search for and actual values
-getConfigData :: FilePath -> IO [(BS.ByteString, BS.ByteString)]
-getConfigData filepath = zip <$> keys <*> content
-    where content = (map (BS.pack . filterLine)) . lines <$> file
-          keys    = (map (BS.pack . keyLine)) . lines <$> file
-          file    = readFile filepath
-
-keyLine :: String -> String
-keyLine = takeWhile (/=':')
+    return $ (read . (takeWhile (/=',')) . (drop 52)) (BSL.unpack $ responseBody response)
 
--- | Filter a line of a file for only the actual data and no descriptors
-filterLine :: String -> String
-filterLine = reverse . (takeWhile (not . (`elem` (" :" :: String)))) . reverse
+-- | Convert a tweet to a percent-encoded url for querying an API
+urlString :: Tweet -> String
+urlString tweet = concat [ "?status="
+                         , BS.unpack (tweetEncode tweet)
+                         , "&trim_user="
+                         , map toLower (show trim)
+                         , (if isJust (_replyID tweet) then "&in_reply_to_status_id=" else "")
+                         , reply ]
+    where trim  = _trimUser tweet
+          reply = maybe "" id (fmap show $ _replyID $ tweet)
 
--- | Convert a byteString to the percent-encoded version
-urlString :: BS.ByteString -> IO String
-urlString content = do
-    return $ "?status=" ++ ((BS.unpack . paramEncode) content) ++ "&trim_user" ++ (map toLower $ show True)
+-- | Percent-ncode the status update so it's fit for a URL
+tweetEncode :: Tweet -> BS.ByteString
+tweetEncode tweet = paramEncode $ handleStr `BS.append` content
+    where content   = BS.pack . _status $ tweet
+          handleStr = BS.pack $ concatMap ((++ " ") . ((++) "@")) hs
+          hs        = _handles tweet
diff --git a/src/Web/Tweet/Exec.hs b/src/Web/Tweet/Exec.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Tweet/Exec.hs
@@ -0,0 +1,66 @@
+-- | Provides IO action that parses command line options and tweets from stdin
+module Web.Tweet.Exec ( exec
+                      , Program (Program)) where
+
+import Web.Tweet
+import Options.Applicative
+import qualified Data.ByteString.Char8 as BS
+import Control.Monad
+import Data.Foldable (fold)
+
+-- | Data type for our program: one optional path to a credential file, (optionally) the number of tweets to make, the id of the status you're replying to, and a list of users you wish to mention.
+data Program = Program { cred :: Maybe FilePath , tweets :: Maybe String, replyId :: Maybe String, replyHandles :: Maybe [String] }
+
+-- | query twitter to post stdin with no fancy options
+fromStdIn :: Int -> FilePath -> IO ()
+fromStdIn = threadStdIn [] Nothing
+
+-- | Threaded tweets from stdIn
+threadStdIn :: [String] -> Maybe Int -> Int -> FilePath -> IO ()
+threadStdIn hs idNum num filepath = do
+    contents <- getContents
+    thread contents hs idNum num filepath
+
+-- | Executes parser
+exec :: IO ()
+exec = execParser opts >>= select
+    where
+        opts = info (helper <*> program)
+            (fullDesc
+            <> progDesc "Tweet from stdin!"
+            <> header "clit - a Command Line Interface Tweeter")
+
+-- | Executes program
+select :: Program -> IO ()
+select (Program Nothing (Just n) Nothing Nothing) = fromStdIn (read n) ".cred"
+select (Program Nothing Nothing Nothing Nothing) = fromStdIn 4 ".cred"
+select (Program (Just file) (Just n) Nothing Nothing) = fromStdIn (read n) file
+select (Program (Just file) Nothing Nothing Nothing) = fromStdIn 4 file
+select (Program Nothing (Just n) (Just id) (Just handles)) = threadStdIn handles (read id) (read n) ".cred"
+select (Program (Just file) (Just n) (Just id) (Just handles)) = threadStdIn handles (pure . read $ id) (read n) file
+select (Program (Just file) Nothing (Just id) (Just handles)) = threadStdIn handles (pure . read $ id) 4 file
+select (Program Nothing (Just n) (Just id) Nothing) = threadStdIn [] (pure . read $ id) (read n) ".cred"
+select (Program Nothing Nothing (Just id) Nothing) = threadStdIn [] (pure . read $ id) 4 ".cred"
+select (Program Nothing Nothing (Just id) (Just handles)) = threadStdIn handles (pure . read $ id) 4 ".cred"
+select (Program (Just file) (Just n) (Just id) Nothing) = threadStdIn [] (pure . read $ id) (read n) file
+
+-- | Parser to return a program datatype
+program :: Parser Program
+program = Program
+    <$> (optional $ strOption
+        (long "cred"
+        <> short 'c'
+        <> metavar "CREDENTIALS"
+        <> help "path to credentials"))
+    <*> (optional $ strOption
+        (long "tweets"
+        <> short 't'
+        <> metavar "NUM"
+        <> help "Number of tweets in a row, default 4"))
+    <*> (optional $ strOption
+        (long "reply"
+        <> short 'r'
+        <> help "id of status to reply to - be sure to include their handle, e.g. @my_build_errors"))
+    <*> (optional $ some $ argument str
+        (metavar "HANDLE1"
+        <> help "handles to include in replies"))
diff --git a/src/Web/Tweet/Sign.hs b/src/Web/Tweet/Sign.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Tweet/Sign.hs
@@ -0,0 +1,29 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Web.Tweet.Sign where
+
+import Web.Tweet.Utils
+import Web.Authenticate.OAuth
+import Network.HTTP.Client
+
+-- | Sign a request using your OAuth dev token.
+-- Uses the IO monad because signatures require a timestamp
+signRequest :: FilePath -> Request -> IO Request
+signRequest filepath req = do
+    o <- oAuth filepath
+    c <- credential filepath
+    signOAuth o c req
+
+-- | Create an OAuth from config data in a file
+oAuth :: FilePath -> IO OAuth
+oAuth filepath = do
+    secret <- (lineByKey "api-sec") <$> getConfigData filepath
+    key <- (lineByKey "api-key") <$> getConfigData filepath
+    let url = "api.twitter.com"
+    return newOAuth { oauthConsumerKey = key , oauthConsumerSecret = secret , oauthServerName = url }
+
+-- | Create a new credential from a token and token secret
+credential :: FilePath -> IO Credential
+credential filepath = newCredential <$> token <*> secretToken
+    where token       = (lineByKey "tok") <$> getConfigData filepath
+          secretToken = (lineByKey "tok-sec") <$> getConfigData filepath
diff --git a/src/Web/Tweet/Types.hs b/src/Web/Tweet/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Tweet/Types.hs
@@ -0,0 +1,27 @@
+{-# LANGUAGE DeriveGeneric   #-}
+{-# LANGUAGE DeriveAnyClass  #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+-- | Exports the `Tweet` type, a datatype for building tweets easily
+module Web.Tweet.Types where
+
+import Data.Aeson
+import GHC.Generics
+import Control.Lens
+import Data.Default
+
+-- | Default value for Bool for trim_user (`True` in our case)
+instance Default Bool where
+    def = True
+
+-- | Data type for our request: consists of the status text, whether to trium user information in the response, the handles to mention, and optionally the id of the status to reply to.
+data Tweet = Tweet
+    { _status   :: String
+    , _trimUser :: Bool
+    , _handles  :: [String]
+    , _replyID  :: Maybe Int
+    } deriving (Generic, Default)
+
+makeLenses ''Tweet
+
+instance ToJSON Tweet where
diff --git a/src/Web/Tweet/Utils.hs b/src/Web/Tweet/Utils.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Tweet/Utils.hs
@@ -0,0 +1,23 @@
+-- | Miscellaneous functions that don't fit the project directly
+module Web.Tweet.Utils where
+
+import qualified Data.ByteString.Char8 as BS
+
+-- | helper function to get the key as read from a file
+keyLinePie :: String -> String
+keyLinePie = takeWhile (/=':')
+
+-- | Pick out a key value from a key
+lineByKey :: BS.ByteString -> [(BS.ByteString, BS.ByteString)] -> BS.ByteString
+lineByKey key = snd . head . (filter (\i -> fst i == key))
+
+-- | Filter a line of a file for only the actual data and no descriptors
+filterLine :: String -> String
+filterLine = reverse . (takeWhile (not . (`elem` (" :" :: String)))) . reverse
+
+-- | Get pairs of "key" to search for and actual values
+getConfigData :: FilePath -> IO [(BS.ByteString, BS.ByteString)]
+getConfigData filepath = zip <$> keys <*> content
+    where content = (map (BS.pack . filterLine)) . lines <$> file
+          keys    = (map (BS.pack . keyLinePie)) . lines <$> file
+          file    = readFile filepath
