diff --git a/Changelog.md b/Changelog.md
deleted file mode 100644
--- a/Changelog.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# 0.3.0.0
-* Add support for cookies
-
-# 0.2.0.3
-* removed the base-noprelude dependency in favor of using a `hiding` mixin
-
-# 0.1.0.0
-* initial release
diff --git a/README.md b/README.md
deleted file mode 100644
--- a/README.md
+++ /dev/null
@@ -1,172 +0,0 @@
-# About
-
-This Haskell library provides a [Polysemy] effect for running HTTP requests
-with [http-client].
-
-# Example
-
-```haskell
-import Polysemy (runM, resourceToIO)
-import qualified Polysemy.Http as Http
-import Polysemy.Http (interpretHttpNative, interpretLogStdout)
-
-main :: IO ()
-main = do
-  result <- runM $
-    resourceToIO $
-    interpretLogStdout $
-    interpretHttpNative $
-    Http.request (Http.get "hackage.haskell.org" "package/polysemy-http")
-  print result
-```
-
-# API
-
-## Request
-
-The effect constructor `Http.request` takes an argument of type
-`Polysemy.Http.Data.Request.Request`:
-
-```haskell
-data Request =
-  Request {
-    _method :: Method,
-    _host :: Host,
-    _port :: Maybe Port,
-    _tls :: Tls,
-    _path :: Path,
-    _headers :: [(HeaderName, HeaderValue)],
-    _cookies :: CookieJar,
-    _query :: [(QueryKey, Maybe QueryValue)],
-    _body :: Body
-  }
-```
-
-Most of these fields are just newtypes, except for `Method`, which is an enum:
-
-```haskell
-data Method =
-  Get | Post | ... | Custom Text
-```
-
-It has an `IsString` instance, so you can just write `"GET"` or `"delete"`.
-
-All `Text` newtypes have `IsString` as well, and they will be converted to
-`CI` and `ByteString` if needed when they are passed to [http-client].
-`Body` is an `LByteString` newtype since that is what [aeson] typically
-produces.
-The port field is intended for nonstandard ports – if it is `Nothing`, the port
-will be determined from `tls`.
-
-## Response
-
-`Http.request` returns `Either HttpError (Response LByteString)`, with
-`Polysemy.Http.Data.Response.Response` looking like this:
-
-```haskell
-data Response b =
-  Response {
-    _status :: Status,
-    _body :: b,
-    _headers :: [Header],
-    _cookies :: CookieJar
-  }
-
-data Header =
-  Header {
-    name :: HeaderName,
-    value :: HeaderValue
-  }
-```
-
-`Status` is from `http-types`, because it has some helpful combinators. Its
-`Header` is just an alias, so this newtype is provided.
-The parameter `b` is intended to allow you to write interpreters that produce
-`Text` or something else, for example for [#testing].
-
-# Streaming
-
-The higher-order constructor `Http.stream` opens and closes the request
-manually and passes the response to a handler function.
-The function `streamResponse` provides a simpler interface for this mechanism
-that runs a loop that passes individual chunks produced by [http-client] to
-a callback handler of type `∀ x . StreamEvent r c h x -> Sem r x` that should
-look like this:
-
-```haskell
-handle ::
-  StreamEvent Double (IO ByteString) Int a ->
-  Sem r a
-handle = \case
-  StreamEvent.Acquire (Response status body headers) ->
-    pure 1
-  StreamEvent.Chunk handle (StreamChunk c) ->
-    pure ()
-  StreamEvent.Result (Response status body headers) handle ->
-    pure 5.5
-  StreamEvent.Release handle ->
-    pure ()
-
-run :: Sem r Double
-run =
-  Http.streamResponse (Request.get "host.com" "path/to/file") handle
-```
-
-If you were e.g. to write the data to disk, you would open the file in the
-`Acquire` block, write the `ByteString` `c` in `Chunk`, and close the file in
-`Release`.
-The parameter `h` could then be `Handle`.
-The callbacks are wrapped in `Resource.bracket`, so `Release` is guaranteed to
-be called (as much as `Resource` is reliable).
-The `Result` block is called when the transfer is complete; its returned value
-is finally returned from `streamHandler.`
-The `handle` is an arbitrary identifier that the user can return from
-`Acquire`; it is not needed for the machinery and may be `()`.
-
-# Entity
-
-The library also provides effects for request and response entity de/encoding,
-`EntityDecode d m a` and `EntityEncode d m a`, making it possible to abstract
-over json implementations or content types using interpreters.
-Since the effects are parameterized by the codec data type, one interpreter per
-type must be used.
-
-Implementations for [aeson] are available as `interpretEntityDecodeAeson` and
-`interpretEntityEncodeAeson`:
-
-```haskell
-import Polysemy (run)
-import qualified Polysemy.Http as Http
-import Polysemy.Http (interpretEntityDecodeAeson)
-
-data Dat { a :: Maybe Int, b :: Text }
-deriving (Show, FromJSON)
-
-main :: IO
-main =
-  print $ run $ interpretEntityDecodeAeson $ Http.decode "{ \"b\": \"hello\" }"
-```
-
-There is not integration with the `Http` effect for this.
-
-# Testing
-
-Polysemy makes it very easy to switch the native interpreter for a mock, and
-there is a convenience interpreter named `interpretHttpStrict` that allows you
-to specify a list of responses and chunks that should be produced:
-
-```haskell
-main :: IO ()
-main = do
-  result <- runM $
-    resourceToIO $
-    interpretLogStdout $
-    interpretHttpStrict [Response (toEnum 200) "foo" []] [] $
-    Http.request (Http.get "hackage.haskell.org" "package/polysemy-http")
-  print result
-```
-
-[Polysemy]: https://hackage.haskell.org/package/polysemy
-[http-client]: https://hackage.haskell.org/package/http-client
-[http-types]: https://hackage.haskell.org/package/http-types
-[aeson]: https://hackage.haskell.org/package/aeson
diff --git a/changelog.md b/changelog.md
new file mode 100644
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,11 @@
+# 0.4.0.0
+* Use `CookieJar` in the `Response` instead of requiring users to extract them from the headers.
+
+# 0.3.0.0
+* Add support for cookies.
+
+# 0.2.0.3
+* Remove the `base-noprelude` dependency in favor of using a `hiding` mixin.
+
+# 0.1.0.0
+* Initial release.
diff --git a/polysemy-http.cabal b/polysemy-http.cabal
--- a/polysemy-http.cabal
+++ b/polysemy-http.cabal
@@ -5,9 +5,9 @@
 -- see: https://github.com/sol/hpack
 
 name:           polysemy-http
-version:        0.4.0.2
+version:        0.4.0.3
 synopsis:       Polysemy effect for http-client
-description:    Please see the README on Github at <https://github.com/tek/polysemy-http>
+description:    See <https://hackage.haskell.org/package/polysemy-http/docs/Polysemy-Http.html>
 category:       Network
 homepage:       https://github.com/tek/polysemy-http#readme
 bug-reports:    https://github.com/tek/polysemy-http/issues
@@ -18,8 +18,8 @@
 license-file:   LICENSE
 build-type:     Simple
 extra-source-files:
-    README.md
-    Changelog.md
+    readme.md
+    changelog.md
 
 source-repository head
   type: git
@@ -111,8 +111,6 @@
     , base ==4.*
     , bytestring
     , case-insensitive ==1.2.*
-    , co-log-core >=0.2.1 && <0.3
-    , co-log-polysemy >=0.0.1.2 && <0.1
     , composition >=1.0.2 && <1.1
     , containers
     , data-default ==0.7.*
@@ -204,8 +202,6 @@
     , base ==4.*
     , bytestring
     , case-insensitive ==1.2.*
-    , co-log-core >=0.2.1 && <0.3
-    , co-log-polysemy >=0.0.1.2 && <0.1
     , composition >=1.0.2 && <1.1
     , containers
     , data-default ==0.7.*
@@ -306,8 +302,6 @@
     , base ==4.*
     , bytestring
     , case-insensitive ==1.2.*
-    , co-log-core >=0.2.1 && <0.3
-    , co-log-polysemy >=0.0.1.2 && <0.1
     , composition >=1.0.2 && <1.1
     , containers
     , data-default ==0.7.*
diff --git a/readme.md b/readme.md
new file mode 100644
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,172 @@
+# About
+
+This Haskell library provides a [Polysemy] effect for running HTTP requests
+with [http-client].
+
+# Example
+
+```haskell
+import Polysemy (runM, resourceToIO)
+import qualified Polysemy.Http as Http
+import Polysemy.Http (interpretHttpNative, interpretLogStdout)
+
+main :: IO ()
+main = do
+  result <- runM $
+    resourceToIO $
+    interpretLogStdout $
+    interpretHttpNative $
+    Http.request (Http.get "hackage.haskell.org" "package/polysemy-http")
+  print result
+```
+
+# API
+
+## Request
+
+The effect constructor `Http.request` takes an argument of type
+`Polysemy.Http.Data.Request.Request`:
+
+```haskell
+data Request =
+  Request {
+    _method :: Method,
+    _host :: Host,
+    _port :: Maybe Port,
+    _tls :: Tls,
+    _path :: Path,
+    _headers :: [(HeaderName, HeaderValue)],
+    _cookies :: CookieJar,
+    _query :: [(QueryKey, Maybe QueryValue)],
+    _body :: Body
+  }
+```
+
+Most of these fields are just newtypes, except for `Method`, which is an enum:
+
+```haskell
+data Method =
+  Get | Post | ... | Custom Text
+```
+
+It has an `IsString` instance, so you can just write `"GET"` or `"delete"`.
+
+All `Text` newtypes have `IsString` as well, and they will be converted to
+`CI` and `ByteString` if needed when they are passed to [http-client].
+`Body` is an `LByteString` newtype since that is what [aeson] typically
+produces.
+The port field is intended for nonstandard ports – if it is `Nothing`, the port
+will be determined from `tls`.
+
+## Response
+
+`Http.request` returns `Either HttpError (Response LByteString)`, with
+`Polysemy.Http.Data.Response.Response` looking like this:
+
+```haskell
+data Response b =
+  Response {
+    _status :: Status,
+    _body :: b,
+    _headers :: [Header],
+    _cookies :: CookieJar
+  }
+
+data Header =
+  Header {
+    name :: HeaderName,
+    value :: HeaderValue
+  }
+```
+
+`Status` is from `http-types`, because it has some helpful combinators. Its
+`Header` is just an alias, so this newtype is provided.
+The parameter `b` is intended to allow you to write interpreters that produce
+`Text` or something else, for example for [#testing].
+
+# Streaming
+
+The higher-order constructor `Http.stream` opens and closes the request
+manually and passes the response to a handler function.
+The function `streamResponse` provides a simpler interface for this mechanism
+that runs a loop that passes individual chunks produced by [http-client] to
+a callback handler of type `∀ x . StreamEvent r c h x -> Sem r x` that should
+look like this:
+
+```haskell
+handle ::
+  StreamEvent Double (IO ByteString) Int a ->
+  Sem r a
+handle = \case
+  StreamEvent.Acquire (Response status body headers) ->
+    pure 1
+  StreamEvent.Chunk handle (StreamChunk c) ->
+    pure ()
+  StreamEvent.Result (Response status body headers) handle ->
+    pure 5.5
+  StreamEvent.Release handle ->
+    pure ()
+
+run :: Sem r Double
+run =
+  Http.streamResponse (Request.get "host.com" "path/to/file") handle
+```
+
+If you were e.g. to write the data to disk, you would open the file in the
+`Acquire` block, write the `ByteString` `c` in `Chunk`, and close the file in
+`Release`.
+The parameter `h` could then be `Handle`.
+The callbacks are wrapped in `Resource.bracket`, so `Release` is guaranteed to
+be called (as much as `Resource` is reliable).
+The `Result` block is called when the transfer is complete; its returned value
+is finally returned from `streamHandler.`
+The `handle` is an arbitrary identifier that the user can return from
+`Acquire`; it is not needed for the machinery and may be `()`.
+
+# Entity
+
+The library also provides effects for request and response entity de/encoding,
+`EntityDecode d m a` and `EntityEncode d m a`, making it possible to abstract
+over json implementations or content types using interpreters.
+Since the effects are parameterized by the codec data type, one interpreter per
+type must be used.
+
+Implementations for [aeson] are available as `interpretEntityDecodeAeson` and
+`interpretEntityEncodeAeson`:
+
+```haskell
+import Polysemy (run)
+import qualified Polysemy.Http as Http
+import Polysemy.Http (interpretEntityDecodeAeson)
+
+data Dat { a :: Maybe Int, b :: Text }
+deriving (Show, FromJSON)
+
+main :: IO
+main =
+  print $ run $ interpretEntityDecodeAeson $ Http.decode "{ \"b\": \"hello\" }"
+```
+
+There is not integration with the `Http` effect for this.
+
+# Testing
+
+Polysemy makes it very easy to switch the native interpreter for a mock, and
+there is a convenience interpreter named `interpretHttpStrict` that allows you
+to specify a list of responses and chunks that should be produced:
+
+```haskell
+main :: IO ()
+main = do
+  result <- runM $
+    resourceToIO $
+    interpretLogStdout $
+    interpretHttpStrict [Response (toEnum 200) "foo" []] [] $
+    Http.request (Http.get "hackage.haskell.org" "package/polysemy-http")
+  print result
+```
+
+[Polysemy]: https://hackage.haskell.org/package/polysemy
+[http-client]: https://hackage.haskell.org/package/http-client
+[http-types]: https://hackage.haskell.org/package/http-types
+[aeson]: https://hackage.haskell.org/package/aeson
