diff --git a/Network/Wreq.hs b/Network/Wreq.hs
--- a/Network/Wreq.hs
+++ b/Network/Wreq.hs
@@ -37,6 +37,10 @@
 module Network.Wreq
     (
     -- * HTTP verbs
+
+    -- ** Sessions
+    -- $session
+
     -- ** GET
       get
     , getWith
@@ -525,6 +529,20 @@
            -> String         -- ^ The body for this 'Form.Part'.
            -> Form.Part
 partString name value = Form.partBS name (encodeUtf8 (T.pack value))
+
+-- $session
+--
+-- The basic HTTP functions ('get', 'post', and so on) in this module
+-- have a few key drawbacks:
+--
+-- * If several requests go to the same server, there is no reuse of
+--   TCP connections.
+--
+-- * There is no management of cookies across multiple requests.
+--
+-- This makes these functions inefficient and verbose for many common
+-- uses.  For greater efficiency, use the "Network.Wreq.Session"
+-- module.
 
 -- $cookielenses
 --
diff --git a/Network/Wreq/Internal/Lens.hs b/Network/Wreq/Internal/Lens.hs
--- a/Network/Wreq/Internal/Lens.hs
+++ b/Network/Wreq/Internal/Lens.hs
@@ -30,6 +30,7 @@
     , assoc
     , assoc2
     , setHeader
+    , maybeSetHeader
     , deleteKey
     ) where
 
@@ -55,8 +56,17 @@
 assoc2 k f = fmap (uncurry ((++) . fmap ((,) k))) .
              _1 (f . fmap snd) . partition ((==k) . fst)
 
+-- | Set a header to the given value, replacing any prior value.
 setHeader :: HeaderName -> S.ByteString -> Request -> Request
 setHeader name value = requestHeaders %~ ((name,value) :) . deleteKey name
+
+-- | Set a header to the given value, but only if the header was not
+-- already set.
+maybeSetHeader :: HeaderName -> S.ByteString -> Request -> Request
+maybeSetHeader name value = requestHeaders %~
+  \hdrs -> case lookup name hdrs of
+             Just _  -> hdrs
+             Nothing -> (name,value) : hdrs
 
 deleteKey :: (Eq a) => a -> [(a,b)] -> [(a,b)]
 deleteKey key = filter ((/= key) . fst)
diff --git a/Network/Wreq/Internal/Types.hs b/Network/Wreq/Internal/Types.hs
--- a/Network/Wreq/Internal/Types.hs
+++ b/Network/Wreq/Internal/Types.hs
@@ -149,13 +149,13 @@
   -- etc.), this field will be used only for the /first/ HTTP request
   -- to be issued during a 'Network.Wreq.Session.Session'. Any changes
   -- changes made for subsequent requests will be ignored.
-  , checkStatus :: 
+  , checkStatus ::
     Maybe (Status -> ResponseHeaders -> CookieJar -> Maybe SomeException)
   -- ^ Function that checks the status code and potentially returns an exception.
   --
   -- This defaults to 'Nothing', which will just use the default of
   -- 'Network.HTTP.Client.Request' which throws a 'StatusException' if the status
-  -- is not 2XX. 
+  -- is not 2XX.
   } deriving (Typeable)
 
 -- | Supported authentication types.
@@ -255,6 +255,10 @@
 -- | A request that is ready to be submitted.
 data Req = Req Mgr Request
 
+-- | Return the URL associated with the given 'Req'.
+--
+-- This includes the port number if not standard, and the query string
+-- if one exists.
 reqURL :: Req -> S.ByteString
 reqURL (Req _ req) = mconcat [
     if https then "https" else "http"
@@ -275,7 +279,8 @@
 -- response.
 type Run body = Req -> IO (Response body)
 
--- | A session that spans multiple requests.
+-- | A session that spans multiple requests.  This is responsible for
+-- cookie management and TCP connection reuse.
 data Session = Session {
       seshCookies :: MVar CookieJar
     , seshManager :: Manager
diff --git a/Network/Wreq/Lens.hs b/Network/Wreq/Lens.hs
--- a/Network/Wreq/Lens.hs
+++ b/Network/Wreq/Lens.hs
@@ -167,7 +167,7 @@
 -- Example (note the use of TLS):
 --
 -- @
---let opts = 'Network.Wreq.defaults' 'Control.Lens.&' 'Lens.auth' 'Control.Lens..~' 'Network.Wreq.basicAuth' \"user\" \"pass\"
+--let opts = 'Network.Wreq.defaults' 'Control.Lens.&' 'Lens.auth' 'Control.Lens.?~' 'Network.Wreq.basicAuth' \"user\" \"pass\"
 --'Network.Wreq.getWith' opts \"https:\/\/httpbin.org\/basic-auth\/user\/pass\"
 -- @
 auth :: Lens' Options (Maybe Auth)
@@ -232,6 +232,10 @@
 checkStatus = TH.checkStatus
 
 -- | A traversal onto the cookie with the given name, if one exists.
+--
+-- N.B. This is an \"illegal\" 'Traversal'': we can change the
+-- 'cookieName' of the associated 'Cookie' so that it differs from the
+-- name provided to this function.
 cookie :: ByteString -> Traversal' Options Cookie
 cookie = TH.cookie
 
diff --git a/Network/Wreq/Session.hs b/Network/Wreq/Session.hs
--- a/Network/Wreq/Session.hs
+++ b/Network/Wreq/Session.hs
@@ -1,5 +1,42 @@
 {-# LANGUAGE RankNTypes, RecordWildCards #-}
 
+-- |
+-- Module      : Network.Wreq
+-- Copyright   : (c) 2014 Bryan O'Sullivan
+--
+-- License     : BSD-style
+-- Maintainer  : bos@serpentine.com
+-- Stability   : experimental
+-- Portability : GHC
+--
+-- The functions in this module use a 'Session' to handle the
+-- following common needs:
+--
+-- * TCP connection reuse.  This is important for performance when
+--   multiple requests go to a single server, particularly if TLS is
+--   being used.
+--
+-- * Transparent cookie management.  Any cookies set by the server
+--   persist from one request to the next.
+--
+--
+-- This module is designed to be used alongside the "Network.Wreq"
+-- module.  Typical usage will look like this:
+--
+-- @
+-- import "Network.Wreq"
+-- import qualified "Network.Wreq.Session" as Sess
+--
+-- main = Sess.'withSession' $ \\sess ->
+--   Sess.'get' sess \"http:\/\/httpbin.org\/get\"
+-- @
+--
+-- We create a 'Session' using 'withSession', then pass the session to
+-- subsequent functions.
+--
+-- Note the use of a qualified import statement, so that we can refer
+-- unambiguously to the 'Session'-specific implementation of HTTP GET.
+
 module Network.Wreq.Session
     (
       Session
@@ -35,9 +72,12 @@
 import qualified Network.Wreq as Wreq
 import qualified Network.Wreq.Internal.Lens as Lens
 
+-- | Create a 'Session', passing it to the given function.  The
+-- 'Session' will no longer be valid after that function returns.
 withSession :: (Session -> IO a) -> IO a
 withSession = withSessionWith defaultManagerSettings
 
+-- | Create a session, using the given manager settings.
 withSessionWith :: HTTP.ManagerSettings -> (Session -> IO a) -> IO a
 withSessionWith settings act = do
   mv <- newMVar $ HTTP.createCookieJar []
@@ -47,42 +87,54 @@
                 , seshRun = runWith
                 }
 
+-- | 'Session'-specific version of 'Network.Wreq.get'.
 get :: Session -> String -> IO (Response L.ByteString)
 get = getWith defaults
 
+-- | 'Session'-specific version of 'Network.Wreq.post'.
 post :: Postable a => Session -> String -> a -> IO (Response L.ByteString)
 post = postWith defaults
 
+-- | 'Session'-specific version of 'Network.Wreq.head_'.
 head_ :: Session -> String -> IO (Response ())
 head_ = headWith defaults
 
+-- | 'Session'-specific version of 'Network.Wreq.options'.
 options :: Session -> String -> IO (Response ())
 options = optionsWith defaults
 
+-- | 'Session'-specific version of 'Network.Wreq.put'.
 put :: Putable a => Session -> String -> a -> IO (Response L.ByteString)
 put = putWith defaults
 
+-- | 'Session'-specific version of 'Network.Wreq.delete'.
 delete :: Session -> String -> IO (Response L.ByteString)
 delete = deleteWith defaults
 
+-- | 'Session'-specific version of 'Network.Wreq.getWith'.
 getWith :: Options -> Session -> String -> IO (Response L.ByteString)
 getWith opts sesh url = run string sesh =<< prepareGet opts url
 
+-- | 'Session'-specific version of 'Network.Wreq.postWith'.
 postWith :: Postable a => Options -> Session -> String -> a
          -> IO (Response L.ByteString)
 postWith opts sesh url payload =
   run string sesh =<< preparePost opts url payload
 
+-- | 'Session'-specific version of 'Network.Wreq.headWith'.
 headWith :: Options -> Session -> String -> IO (Response ())
 headWith opts sesh url = run ignore sesh =<< prepareHead opts url
 
+-- | 'Session'-specific version of 'Network.Wreq.optionsWith'.
 optionsWith :: Options -> Session -> String -> IO (Response ())
 optionsWith opts sesh url = run ignore sesh =<< prepareOptions opts url
 
+-- | 'Session'-specific version of 'Network.Wreq.putWith'.
 putWith :: Putable a => Options -> Session -> String -> a
         -> IO (Response L.ByteString)
 putWith opts sesh url payload = run string sesh =<< preparePut opts url payload
 
+-- | 'Session'-specific version of 'Network.Wreq.deleteWith'.
 deleteWith :: Options -> Session -> String -> IO (Response L.ByteString)
 deleteWith opts sesh url = run string sesh =<< prepareDelete opts url
 
diff --git a/Network/Wreq/Types.hs b/Network/Wreq/Types.hs
--- a/Network/Wreq/Types.hs
+++ b/Network/Wreq/Types.hs
@@ -142,5 +142,5 @@
 
 payload :: S.ByteString -> HTTP.RequestBody -> Request -> IO Request
 payload ct body req = AWS.addTmpPayloadHashHeader $ req
-                    & Lens.setHeader "Content-Type" ct
+                    & Lens.maybeSetHeader "Content-Type" ct
                     & Lens.requestBody .~ body
diff --git a/changelog b/changelog
deleted file mode 100644
--- a/changelog
+++ /dev/null
@@ -1,19 +0,0 @@
--*- markdown -*-
-
-2014-12-02 0.3.0.0
-
-* Support for Amazon Web Services request signing
-
-* New customMethod, customMethodWith functions allow use of arbitrary
-  HTTP verbs
-
-* httpProxy, basicAuth, oauth2Bearer, oauth2Token: removed Maybe from
-  result types, changed documentation to suggest use of (?~)
-
-2014-08-25 0.2.0.0
-
-* Support for lens 4.4
-
-2014-04-22 0.1.0.0
-
-* Initial release.
diff --git a/changelog.md b/changelog.md
new file mode 100644
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,23 @@
+-*- markdown -*-
+
+2014-12-11 0.3.0.1
+
+* Bump lower bound on http-client to 0.3.0.1
+
+2014-12-02 0.3.0.0
+
+* Support for Amazon Web Services request signing
+
+* New customMethod, customMethodWith functions allow use of arbitrary
+  HTTP verbs
+
+* httpProxy, basicAuth, oauth2Bearer, oauth2Token: removed Maybe from
+  result types, changed documentation to suggest use of (?~)
+
+2014-08-25 0.2.0.0
+
+* Support for lens 4.4
+
+2014-04-22 0.1.0.0
+
+* Initial release.
diff --git a/examples/SimpleSession.hs b/examples/SimpleSession.hs
new file mode 100644
--- /dev/null
+++ b/examples/SimpleSession.hs
@@ -0,0 +1,24 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+import Control.Lens
+import Network.Wreq
+import qualified Network.Wreq.Session as S
+
+main :: IO ()
+main = S.withSession $ \sess -> do
+  -- Our first request causes the httpbin.org server to set a cookie
+  -- in its response.
+
+  S.get sess "http://httpbin.org/cookies/set?name=hi"
+
+  -- The session value manages both cookies and HTTP connection reuse
+  -- for us.  When we issue the second request, it should
+  -- transparently reuse the same connection, and also send the
+  -- cookies that we set during the first request.
+
+  r2 <- S.post sess "http://httpbin.org/post" ["a" := (3 :: Int)]
+
+  -- And here's where we verify that the cookie is still set on the
+  -- second request.
+
+  print $ r2 ^. responseCookie "name" . cookieValue
diff --git a/examples/wreq-examples.cabal b/examples/wreq-examples.cabal
--- a/examples/wreq-examples.cabal
+++ b/examples/wreq-examples.cabal
@@ -26,6 +26,17 @@
     text,
     wreq
 
+
+executable wreq-example-simple-session
+  main-is:        SimpleSession.hs
+  ghc-options:    -Wall -fwarn-tabs -threaded
+  default-language: Haskell98
+
+  build-depends:
+    base >= 4.5 && < 5,
+    lens,
+    wreq
+
 executable upload-paste
   main-is:          UploadPaste.hs
   ghc-options:      -Wall -fwarn-tabs -threaded
diff --git a/tests/UnitTests.hs b/tests/UnitTests.hs
--- a/tests/UnitTests.hs
+++ b/tests/UnitTests.hs
@@ -11,7 +11,7 @@
 import Control.Exception (Exception, toException)
 import Control.Lens ((^.), (^?), (.~), (?~), (&))
 import Control.Monad (unless, void)
-import Data.Aeson (Value(..), object)
+import Data.Aeson
 import Data.Aeson.Lens (key)
 import Data.ByteString (ByteString)
 import Data.Char (toUpper)
@@ -113,6 +113,41 @@
   r <- put (site "/put") ("wibble" :: ByteString)
   assertEqual "PUT succeeds" status200 (r ^. responseStatus)
 
+data SolrAdd = SolrAdd
+    { doc          :: String
+    , boost        :: Float
+    , overwrite    :: Bool
+    , commitWithin :: Integer
+    }
+
+instance ToJSON SolrAdd where
+    toJSON (SolrAdd doc boost overwrite commitWithin) =
+        object
+            [
+                "add" .= object
+                [ "doc" .= toJSON doc
+                , "boost" .= boost
+                , "overwrite" .= overwrite
+                , "commitWithin" .= commitWithin
+                ]
+            ]
+
+solrAdd :: SolrAdd
+solrAdd = SolrAdd "wibble" 1.0 True 10000
+
+jsonPut Verb{..} site = do
+  r <- put (site "/put") $ toJSON solrAdd
+  assertEqual "toJSON PUT request has correct Content-Type header"
+    (Just "application/json")
+    (r ^. responseBody ^? key "headers" . key "Content-Type")
+
+byteStringPut Verb{..} site = do
+  let opts = defaults & header "Content-Type" .~ ["application/json"]
+  r <- putWith opts (site "/put") $ encode solrAdd
+  assertEqual "ByteString PUT request has correct Content-Type header"
+    (Just "application/json")
+    (r ^. responseBody ^? key "headers" . key "Content-Type")
+
 basicDelete Verb{..} site = do
   r <- delete (site "/delete")
   assertEqual "DELETE succeeds" status200 (r ^. responseStatus)
@@ -181,16 +216,16 @@
   r <- getWith opts (site "/status/404")
   assertThrows "Non 404 throws error" inspect $
     getWith opts (site "/get")
-  assertEqual "Status 404" 
+  assertEqual "Status 404"
     404
     (r ^. responseStatus . statusCode)
-  where 
-    customCs (Status 404 _) _ _ = Nothing 
+  where
+    customCs (Status 404 _) _ _ = Nothing
     customCs s h cj             = Just . toException . StatusCodeException s h $ cj
 
     inspect e = case e of
-      (StatusCodeException (Status sc _) _ _) -> 
-        assertEqual "200 Status Error" sc 200 
+      (StatusCodeException (Status sc _) _ _) ->
+        assertEqual "200 Status Error" sc 200
 
 getGzip Verb{..} site = do
   r <- get (site "/gzip")
@@ -266,6 +301,8 @@
     , testCase "params" $ getParams verb site
     , testCase "headers" $ getHeaders verb site
     , testCase "gzip" $ getGzip verb site
+    , testCase "json put" $ jsonPut verb site
+    , testCase "bytestring put" $ byteStringPut verb site
     , testCase "cookiesSet" $ cookiesSet verb site
     , testCase "getWithManager" $ getWithManager site
     , testCase "cookieSession" $ cookieSession site
diff --git a/wreq.cabal b/wreq.cabal
--- a/wreq.cabal
+++ b/wreq.cabal
@@ -1,5 +1,5 @@
 name:                wreq
-version:             0.3.0.0
+version:             0.3.0.1
 synopsis:            An easy-to-use HTTP client library.
 description:
   .
@@ -40,7 +40,7 @@
 extra-source-files:
   README.md
   TODO.md
-  changelog
+  changelog.md
   examples/*.cabal
   examples/*.hs
   www/*.css
@@ -100,7 +100,7 @@
     exceptions >= 0.5,
     ghc-prim,
     hashable,
-    http-client >= 0.4.3,
+    http-client >= 0.4.6,
     http-client-tls >= 0.2,
     http-types >= 0.8,
     lens >= 4.5,
diff --git a/www/index.md b/www/index.md
--- a/www/index.md
+++ b/www/index.md
@@ -34,13 +34,24 @@
 
 # Whirlwind tour
 
+All of the examples that follow assume that you are using the
+[`OverloadedStrings`](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/type-class-extensions.html#overloaded-strings)
+language extension, which you can enable in `ghci` as follows:
+
 ~~~~ {.haskell}
+ghci> :set -XOverloadedStrings
+~~~~
+
+And now let's get started.
+
+~~~~ {.haskell}
 ghci> import Network.Wreq
 ghci> r <- get "http://httpbin.org/get"
 ~~~~
 
-Its `lens`-based API is easy to learn (the tutorial walks you through
-the [basics of lenses](tutorial.html#a-quick-lens-backgrounder) and
+The `wreq` library's `lens`-based API is easy to learn (the tutorial
+walks you through the
+[basics of lenses](tutorial.html#a-quick-lens-backgrounder)) and
 powerful to work with.
 
 ~~~~ {.haskell}
@@ -55,7 +66,7 @@
 ~~~~ {.haskell}
 ghci> let opts = defaults & param "q" .~ ["tetris"]
                           & param "language" .~ ["haskell"]
-ghci> r <- getWith opts "https://api.github.com/search/code"
+ghci> r <- getWith opts "https://api.github.com/search/repositories"
 ~~~~
 
 Haskell-to-JSON interoperation is seamless.
diff --git a/www/tutorial.md b/www/tutorial.md
--- a/www/tutorial.md
+++ b/www/tutorial.md
@@ -414,7 +414,10 @@
 "bar"
 ~~~~
 
+To make cookies even easier to deal with, we'll want to
+[use the `Session` API](#session), but we'll come back to that later.
 
+
 # Authentication
 
 The `wreq` library supports both basic authentication and OAuth2
@@ -540,3 +543,54 @@
 authentication during a session, to avoid the need for an
 unauthenticated failure followed by an authenticated success if we
 visit the same endpoint repeatedly.)
+
+
+# Handling multiple HTTP requests
+
+<a id="session">For non-trivial applications</a>, we'll always want to
+use a
+[`Session`](http://hackage.haskell.org/package/wreq/docs/Network-Wreq-Session.html#t:Session)
+to efficiently and correctly handle multiple requests.
+
+The `Session` API provides two important features:
+
+* When we issue multiple HTTP requests to the same server, a `Session`
+  will reuse TCP and TLS connections for us.  (The simpler API we've
+  discussed so far does not do this.)  This greatly improves
+  efficiency.
+
+* A `Session` transparently manages HTTP cookies.  (We can manage them
+  by hand, but it's awkward and verbose, so we won't cover it in this
+  tutorial.)
+
+Here's a complete example.
+
+~~~~ {.haskell}
+{-# LANGUAGE OverloadedStrings #-}
+
+import Control.Lens
+import Network.Wreq
+import qualified Network.Wreq.Session as S
+
+main :: IO ()
+main = S.withSession $ \sess -> do
+  -- First request: tell the server to set a cookie
+  S.get sess "http://httpbin.org/cookies/set?name=hi"
+
+  -- Second request: the cookie should still be set afterwards.
+  r <- S.post sess "http://httpbin.org/post" ["a" := (3 :: Int)]
+  print $ r ^. responseCookie "name" . cookieValue
+~~~~
+
+The key differences from the basic API are as follows.
+
+* We import the
+  [`Network.Wreq.Session`](http://hackage.haskell.org/package/wreq/docs/Network-Wreq-Session.html)
+  module qualified, and we'll identify its functions by prefixing them
+  with "`S.`".
+
+* To create a `Session`, we use `S.withSession`. It calls our code
+  with `sess`, the `Session` value we'll use.
+
+* Instead of `get` and `post`, we call the `Session`-specific
+  versions, [`S.get`](http://hackage.haskell.org/package/wreq/docs/Network-Wreq-Session.html#v:get) and [`S.post`](http://hackage.haskell.org/package/wreq/docs/Network-Wreq-Session.html#v:post), and pass `sess` to each of them.
