diff --git a/default.nix b/default.nix
index 04193ed1586856a9b21d7403a0cde175071b3344..046aae00876256588381190b25c2a168134a8e4e 100644
--- a/default.nix
+++ b/default.nix
@@ -1,2 +1,2 @@
-{ pkgs ? import ./nixpkgs.nix { }, hypothesisProfile ? null, collectCoverage ? false, testSuite ? null, trialArgs ? [] }:
+{ pkgs ? import ./nixpkgs.nix { }, hypothesisProfile ? null, collectCoverage ? false, testSuite ? null, trialArgs ? null }:
 pkgs.python27Packages.zkapauthorizer.override { inherit hypothesisProfile collectCoverage testSuite trialArgs; }
diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py
index 1939478bbba918a83170d061818cc8a882047855..21e0189b2e899564daebf3a07e9e6c3d27e5bbe6 100644
--- a/src/_zkapauthorizer/controller.py
+++ b/src/_zkapauthorizer/controller.py
@@ -395,9 +395,13 @@ class RistrettoRedeemer(object):
             self._log.failure("Parsing redeem response failed", response=response)
             raise
 
-        if result.get(u"failed", False):
-            if result.get(u"reason", None) == u"double-spend":
+        success = result.get(u"success", False)
+        if not success:
+            reason = result.get(u"reason", None)
+            if reason == u"double-spend":
                 raise AlreadySpent(voucher)
+            elif reason == u"unpaid":
+                raise Unpaid(voucher)
 
         self._log.info("Redeemed: {public-key} {proof} {signatures}", **result)
 
diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py
index 1601d7054ca7f74bbb6553f18e2da20d4ebb3551..fb7e61aab584103c48ffee628e8835d28b73d9bb 100644
--- a/src/_zkapauthorizer/resource.py
+++ b/src/_zkapauthorizer/resource.py
@@ -196,7 +196,7 @@ class _VoucherCollection(Resource):
         application_json(request)
         return dumps({
             u"vouchers": list(
-                voucher.marshal()
+                self._controller.incorporate_transient_state(voucher).marshal()
                 for voucher
                 in self._store.list()
             ),
@@ -211,7 +211,6 @@ class _VoucherCollection(Resource):
             voucher = self._store.get(voucher)
         except KeyError:
             return NoResource()
-        # TODO Apply the same treatment to the list result
         return VoucherView(self._controller.incorporate_transient_state(voucher))
 
 
diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py
index 22d2f544783374379d4ca340b2170d6d468f7d1b..653574423654bd9c5d0cb73da3c37f4b01406d4d 100644
--- a/src/_zkapauthorizer/tests/test_client_resource.py
+++ b/src/_zkapauthorizer/tests/test_client_resource.py
@@ -776,6 +776,59 @@ class VoucherTests(TestCase):
         A ``GET`` to the ``VoucherCollection`` itself returns a list of existing
         vouchers.
         """
+        return self._test_list_vouchers(
+            get_config,
+            now,
+            vouchers,
+            Equals({
+                u"vouchers": list(
+                    Voucher(
+                        voucher,
+                        created=now,
+                        state=Redeemed(
+                            finished=now,
+                            # Value duplicated from
+                            # PaymentController.redeem
+                            # default.  Should do this better.
+                            token_count=100,
+                        ),
+                    ).marshal()
+                    for voucher
+                    in vouchers
+                ),
+            }),
+        )
+
+    @given(
+        tahoe_configs(client_unpaidredeemer_configurations()),
+        datetimes(),
+        lists(vouchers(), unique=True),
+    )
+    def test_list_vouchers_transient_states(self, get_config, now, vouchers):
+        """
+        A ``GET`` to the ``VoucherCollection`` itself returns a list of existing
+        vouchers including state information that reflects transient states.
+        """
+        return self._test_list_vouchers(
+            get_config,
+            now,
+            vouchers,
+            Equals({
+                u"vouchers": list(
+                    Voucher(
+                        voucher,
+                        created=now,
+                        state=Unpaid(
+                            finished=now,
+                        ),
+                    ).marshal()
+                    for voucher
+                    in vouchers
+                ),
+            }),
+        )
+
+    def _test_list_vouchers(self, get_config, now, vouchers, match_response_object):
         # Hypothesis causes our test case instances to be re-used many times
         # between setUp and tearDown.  Avoid re-using the same temporary
         # directory for every Hypothesis iteration because this test leaves
@@ -817,23 +870,7 @@ class VoucherTests(TestCase):
                     AfterPreprocessing(
                         json_content,
                         succeeded(
-                            Equals({
-                                u"vouchers": list(
-                                    Voucher(
-                                        voucher,
-                                        created=now,
-                                        state=Redeemed(
-                                            finished=now,
-                                            # Value duplicated from
-                                            # PaymentController.redeem
-                                            # default.  Should do this better.
-                                            token_count=100,
-                                        ),
-                                    ).marshal()
-                                    for voucher
-                                    in vouchers
-                                ),
-                            }),
+                            match_response_object,
                         ),
                     ),
                 ),
diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py
index 31be5bf25240bcb663e260c297d3bfbc419522b4..86bcd8fa54d0ee95c13eefb6b748a8df3bd40f9b 100644
--- a/src/_zkapauthorizer/tests/test_controller.py
+++ b/src/_zkapauthorizer/tests/test_controller.py
@@ -100,6 +100,7 @@ from ..controller import (
     RistrettoRedeemer,
     PaymentController,
     AlreadySpent,
+    Unpaid,
 )
 
 from ..model import (
@@ -282,6 +283,32 @@ class RistrettoRedeemerTests(TestCase):
             ),
         )
 
+    @given(voucher_objects(), integers(min_value=1, max_value=100))
+    def test_redemption_denied_unpaid(self, voucher, num_tokens):
+        """
+        If the issuer declines to allow the voucher to be redeemed and gives a
+        reason that the voucher has not been paid for, ``RistrettoRedeem``
+        returns a ``Deferred`` that fires with a ``Failure`` wrapping
+        ``Unpaid``.
+        """
+        issuer = UnpaidRedemption()
+        treq = treq_for_loopback_ristretto(issuer)
+        redeemer = RistrettoRedeemer(treq, NOWHERE)
+        random_tokens = redeemer.random_tokens_for_voucher(voucher, num_tokens)
+        d = redeemer.redeem(
+            voucher,
+            random_tokens,
+        )
+        self.assertThat(
+            d,
+            failed(
+                AfterPreprocessing(
+                    lambda f: f.value,
+                    IsInstance(Unpaid),
+                ),
+            ),
+        )
+
     @given(voucher_objects(), integers(min_value=1, max_value=100))
     def test_bad_ristretto_redemption(self, voucher, num_tokens):
         """
@@ -439,8 +466,20 @@ class AlreadySpentRedemption(Resource):
         if request.requestHeaders.getRawHeaders(b"content-type") != ["application/json"]:
             return bad_content_type(request)
 
-        return bad_request(request, {u"failed": True, u"reason": u"double-spend"})
+        return bad_request(request, {u"success": False, u"reason": u"double-spend"})
+
+
+class UnpaidRedemption(Resource):
+    """
+    An ``UnpaidRedemption`` simulates the Ristretto redemption server but
+    always refuses to allow vouchers to be redeemed and reports an error that
+    the voucher has not been paid for.
+    """
+    def render_POST(self, request):
+        if request.requestHeaders.getRawHeaders(b"content-type") != ["application/json"]:
+            return bad_content_type(request)
 
+        return bad_request(request, {u"success": False, u"reason": u"unpaid"})
 
 
 class RistrettoRedemption(Resource):
diff --git a/tahoe-lafs.nix b/tahoe-lafs.nix
index 5dd929dadf082dd8a657515aa1f4e4f2bb642228..c75f6dba03f346328446bf7256adf6900568d490 100644
--- a/tahoe-lafs.nix
+++ b/tahoe-lafs.nix
@@ -2,7 +2,7 @@
 , twisted, foolscap, nevow, zfec
 , setuptools, setuptoolsTrial, pyasn1, zope_interface
 , service-identity, pyyaml, magic-wormhole, treq, appdirs
-, eliot, autobahn, cryptography
+, beautifulsoup4, eliot, autobahn, cryptography
 }:
 python.pkgs.buildPythonPackage rec {
   version = "1.14.0.dev";
@@ -10,10 +10,10 @@ python.pkgs.buildPythonPackage rec {
   src = fetchFromGitHub {
     owner = "LeastAuthority";
     repo = "tahoe-lafs";
-    # HEAD of an integration branch for all of the storage plugin stuff.  Last
-    # updated October 4 2019.
-    rev = "8c1f536ba4fbc01f3bc5f08412edbefc56ff7037";
-    sha256 = "17d7pkbsgss3rhqf7ac7ylzbddi555rnkzz48zjqwq1zx1z2jhy6";
+    # A branch of master with the storage plugin web resource reuse issue
+    # resolved.  https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3265
+    rev = "1fef61981940bbd63ffc4242c3b589258622d117";
+    sha256 = "0kgkg7wd0nkj8f5p46341vjkr6nz3kf0fimd44d9kypm4rn8xczv";
   };
 
   postPatch = ''
@@ -33,8 +33,7 @@ python.pkgs.buildPythonPackage rec {
     twisted foolscap nevow zfec appdirs
     setuptoolsTrial pyasn1 zope_interface
     service-identity pyyaml magic-wormhole treq
-
-    eliot autobahn cryptography setuptools
+    beautifulsoup4 eliot autobahn cryptography setuptools
   ];
 
   checkInputs = with python.pkgs; [