diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Change log
 
+## v0.0.12 (2015-03-25)
+
+-   Combined `/pointfree` and `/pointful` endpoints into `/convert`.
+
 ## v0.0.11 (2015-03-23)
 
 -   Added permalinks by storing the input in the URL hash and reading it on
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@
 Blunt is a web front end to the [pointfree][] and [pointful][] libraries. While
 you can install and run it locally, there's no real reason to prefer it over
 the `pointfree` and `pointful` executables. Instead, use the hosted version:
-<https://evening-thicket-5270.herokuapp.com>.
+<https://blunt.herokuapp.com>.
 
 ## Install
 
diff --git a/blunt.cabal b/blunt.cabal
--- a/blunt.cabal
+++ b/blunt.cabal
@@ -1,5 +1,5 @@
 name: blunt
-version: 0.0.11
+version: 0.0.12
 cabal-version: >=1.10
 build-type: Simple
 license: MIT
@@ -22,6 +22,7 @@
         Blunt
     build-depends:
         base ==4.*,
+        aeson ==0.8.*,
         bytestring -any,
         http-types -any,
         pointful >=1.0.6 && <2,
diff --git a/library/Blunt.hs b/library/Blunt.hs
--- a/library/Blunt.hs
+++ b/library/Blunt.hs
@@ -3,8 +3,8 @@
 module Blunt where
 
 import Control.Exception (SomeException, evaluate, handle)
+import Data.Aeson (ToJSON, (.=), encode, object, toJSON)
 import Data.ByteString.Char8 (unpack)
-import Data.ByteString.Lazy (fromStrict)
 import Data.ByteString.Lazy.Char8 (pack)
 import Lambdabot.Pointful (pointful)
 import Network.HTTP.Types (notFound404, ok200)
@@ -27,8 +27,7 @@
 route :: Request -> Action
 route request = case (requestMethod request, pathInfo request) of
     ("GET", []) -> indexAction
-    ("GET", ["pointfree"]) -> pointfreeAction
-    ("GET", ["pointful"]) -> pointfulAction
+    ("GET", ["convert"]) -> convertAction
     _ -> notFoundAction
 
 indexAction :: Action
@@ -37,28 +36,35 @@
         body = pack html
     return (responseLBS ok200 headers body)
 
-pointfreeAction :: Action
-pointfreeAction request = do
-    let params = queryString request
-        input = case lookup "input" params of
-            Just (Just param) -> param
-            _ -> ""
-    output <- safePointfree (unpack input)
-    let headers = [("Content-Type", "text/plain; charset=utf-8")]
-        body = if null output
-            then fromStrict input
-            else pack (unlines output)
-    return (responseLBS ok200 headers body)
+data Result = Result
+    { resultInput :: String
+    , resultPointfree :: [String]
+    , resultPointful :: String
+    } deriving (Read, Show)
 
-pointfulAction :: Action
-pointfulAction request = do
-    let params = queryString request
-        input = case lookup "input" params of
-            Just (Just param) -> param
+instance ToJSON Result where
+    toJSON result = object
+        [ "input" .= resultInput result
+        , "pointfree" .= resultPointfree result
+        , "pointful" .= resultPointful result
+        ]
+
+convertAction :: Action
+convertAction request = do
+    let input = case lookup "input" (queryString request) of
+            Just (Just param) -> unpack param
             _ -> ""
-        output = pointful (unpack input)
-    let headers = [("Content-Type", "text/plain; charset=utf-8")]
-        body = pack output
+
+    pf <- safePointfree input
+    let pl = pointful input
+        result = Result
+            { resultInput = input
+            , resultPointfree = pf
+            , resultPointful = pl
+            }
+
+    let headers = [("Content-Type", "application/json")]
+        body = encode result
     return (responseLBS ok200 headers body)
 
 notFoundAction :: Action
@@ -194,34 +200,24 @@
     , "    window.location.replace('#input=' + input.value);"
     , "  };"
     , ""
-    , "  var updatePointfree = function () {"
+    , "  var updateOutput = function () {"
     , "    var request = new XMLHttpRequest();"
     , ""
     , "    request.onreadystatechange = function () {"
     , "      if (request.readyState === 4 && request.status === 200) {"
-    , "        pointfree.textContent = request.response;"
-    , "      }"
-    , "    };"
-    , "    request.open('GET', '/pointfree?input=' + encodeURIComponent(input.value));"
-    , "    request.send();"
-    , "  };"
-    , ""
-    , "  var updatePointful = function () {"
-    , "    var request = new XMLHttpRequest();"
+    , "        var response = JSON.parse(request.response);"
     , ""
-    , "    request.onreadystatechange = function () {"
-    , "      if (request.readyState === 4 && request.status === 200) {"
-    , "        pointful.textContent = request.response;"
+    , "        pointfree.textContent = response.pointfree.join('\\n');"
+    , "        pointful.textContent = response.pointful;"
     , "      }"
     , "    };"
-    , "    request.open('GET', '/pointful?input=' + encodeURIComponent(input.value));"
+    , "    request.open('GET', '/convert?input=' + encodeURIComponent(input.value));"
     , "    request.send();"
     , "  };"
     , ""
     , "  input.oninput = function (_event) {"
     , "    updateHash();"
-    , "    updatePointfree();"
-    , "    updatePointful();"
+    , "    updateOutput();"
     , "  };"
     , ""
     , "  if (window.location.hash.indexOf('#input=') === 0) {"
