diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py
index 5c387d4573c84eaedc4c184bbc984d7172aab1ba..ba49e3525a44dd9aeaf4da82828a53804b57f2ab 100644
--- a/src/_zkapauthorizer/controller.py
+++ b/src/_zkapauthorizer/controller.py
@@ -60,6 +60,8 @@ from .model import (
     UnblindedToken,
     Voucher,
     Pass,
+    Pending,
+    Unpaid as model_Unpaid,
 )
 
 
@@ -70,6 +72,14 @@ class AlreadySpent(Exception):
     """
 
 
+class Unpaid(Exception):
+    """
+    An attempt was made to redeem a voucher which has not yet been paid for.
+
+    The redemption attempt may be automatically retried at some point.
+    """
+
+
 class IRedeemer(Interface):
     """
     An ``IRedeemer`` can exchange a voucher for one or more passes.
@@ -179,6 +189,24 @@ class DoubleSpendRedeemer(object):
         return fail(AlreadySpent(voucher))
 
 
+@implementer(IRedeemer)
+@attr.s
+class UnpaidRedeemer(object):
+    """
+    An ``UnpaidRedeemer`` pretends to try to redeem vouchers for ZKAPs but
+    always fails with an error indicating the voucher has not been paid for.
+    """
+    @classmethod
+    def make(cls, section_name, node_config, announcement, reactor):
+        return cls()
+
+    def random_tokens_for_voucher(self, voucher, count):
+        return dummy_random_tokens(voucher, count)
+
+    def redeem(self, voucher, random_tokens):
+        return fail(Unpaid(voucher))
+
+
 def dummy_random_tokens(voucher, count):
     return list(
         RandomToken(u"{}-{}".format(voucher.number, n))
@@ -419,12 +447,19 @@ class PaymentController(object):
          pass construction), the controller hands them to the data store with
          the voucher.  The data store marks the voucher as redeemed and stores
          the unblinded tokens for use by the storage client.
+
+    :ivar dict[voucher, datetime] _unpaid: A mapping from voucher identifiers
+        which have recently failed a redemption attempt due to an unpaid
+        response from the redemption server to timestamps when the failure was
+        observed.
     """
     _log = Logger()
 
     store = attr.ib()
     redeemer = attr.ib()
 
+    _unpaid = attr.ib(default=attr.Factory(dict))
+
     def redeem(self, voucher, num_tokens=100):
         """
         :param unicode voucher: A voucher to redeem.
@@ -473,6 +508,12 @@ class PaymentController(object):
                 voucher=voucher,
             )
             self.store.mark_voucher_double_spent(voucher)
+        elif reason.check(Unpaid):
+            self._log.error(
+                "Voucher {voucher} reported as not paid for during redemption.",
+                voucher=voucher,
+            )
+            self._unpaid[voucher] = self.store.now()
         else:
             self._log.failure(
                 "Redeeming random tokens for a voucher ({voucher}) failed.",
@@ -485,6 +526,20 @@ class PaymentController(object):
         self._log.failure("Redeeming random tokens for a voucher ({voucher}) encountered error.", reason, voucher=voucher)
         return None
 
+    def incorporate_transient_state(self, voucher):
+        """
+        Create a new ``Voucher`` which represents the given voucher but which also
+        incorporates relevant transient state known to the controller.  For
+        example, if a redemption attempt is current in progress, this is
+        incorporated.
+        """
+        if isinstance(voucher.state, Pending) and voucher.number in self._unpaid:
+            return attr.evolve(
+                voucher,
+                state=model_Unpaid(finished=self._unpaid[voucher.number]),
+            )
+        return voucher
+
 
 def get_redeemer(plugin_name, node_config, announcement, reactor):
     section_name = u"storageclient.plugins.{}".format(plugin_name)
@@ -500,5 +555,6 @@ _REDEEMERS = {
     u"non": NonRedeemer.make,
     u"dummy": DummyRedeemer.make,
     u"double-spend": DoubleSpendRedeemer.make,
+    u"unpaid": UnpaidRedeemer.make,
     u"ristretto": RistrettoRedeemer.make,
 }
diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py
index c2a6f79b47dc6e0aeae12933dd91ce89c7b20e9a..95780c8e46095a8a7bf6657b42c98cb96688ea8a 100644
--- a/src/_zkapauthorizer/model.py
+++ b/src/_zkapauthorizer/model.py
@@ -480,6 +480,16 @@ class DoubleSpend(object):
     finished = attr.ib(validator=attr.validators.instance_of(datetime))
 
 
+@attr.s(frozen=True)
+class Unpaid(object):
+    """
+    This is a non-persistent state in which a voucher exists when the database
+    state is **pending** but the most recent redemption attempt has failed due
+    to lack of payment.
+    """
+    finished = attr.ib(validator=attr.validators.instance_of(datetime))
+
+
 @attr.s
 class Voucher(object):
     """
@@ -553,8 +563,12 @@ class Voucher(object):
                 finished=parse_datetime(state_json[u"finished"]),
                 token_count=state_json[u"token-count"],
             )
+        elif state_name == u"unpaid":
+            state = Unpaid(
+                finished=parse_datetime(state_json[u"finished"]),
+            )
         else:
-            raise ValueError("Unrecognized state {}".format(state_json))
+            raise ValueError("Unrecognized state {!r}".format(state_json))
 
         return cls(
             number=values[u"number"],
@@ -587,6 +601,13 @@ class Voucher(object):
                 u"finished": self.state.finished.isoformat(),
                 u"token-count": self.state.token_count,
             }
+        elif isinstance(self.state, Unpaid):
+            state = {
+                u"name": u"unpaid",
+                u"finished": self.state.finished.isoformat(),
+            }
+        else:
+            raise ValueError("Unrecognized state {!r}".format(self.state.__class__))
         return {
             u"number": self.number,
             u"created": None if self.created is None else self.created.isoformat(),
diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py
index 9d13a02193ee85703e4e1dcb5b53f00c706c2dae..1601d7054ca7f74bbb6553f18e2da20d4ebb3551 100644
--- a/src/_zkapauthorizer/resource.py
+++ b/src/_zkapauthorizer/resource.py
@@ -211,7 +211,8 @@ class _VoucherCollection(Resource):
             voucher = self._store.get(voucher)
         except KeyError:
             return NoResource()
-        return VoucherView(voucher)
+        # TODO Apply the same treatment to the list result
+        return VoucherView(self._controller.incorporate_transient_state(voucher))
 
 
 def is_syntactic_voucher(voucher):
diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py
index d990256a2e745db3e3f1debb1fc89e811e02bfb4..9d1ff652d6bee25e20f8bef11e9667abd42858dc 100644
--- a/src/_zkapauthorizer/tests/strategies.py
+++ b/src/_zkapauthorizer/tests/strategies.py
@@ -191,13 +191,22 @@ def client_dummyredeemer_configurations():
 
 def client_doublespendredeemer_configurations():
     """
-    Build DummyRedeemer-using configuration values for the client-side plugin.
+    Build DoubleSpendRedeemer-using configuration values for the client-side plugin.
     """
     return just({
         u"redeemer": u"double-spend",
     })
 
 
+def client_unpaidredeemer_configurations():
+    """
+    Build UnpaidRedeemer-using configuration values for the client-side plugin.
+    """
+    return just({
+        u"redeemer": u"unpaid",
+    })
+
+
 def client_nonredeemer_configurations():
     """
     Build NonRedeemer-using configuration values for the client-side plugin.
diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py
index c63961aed8f0469670921db93686016f605ea82d..cfc9a8c2d0a77615f390896c211a3a906a92bf07 100644
--- a/src/_zkapauthorizer/tests/test_client_resource.py
+++ b/src/_zkapauthorizer/tests/test_client_resource.py
@@ -115,6 +115,7 @@ from ..model import (
     Pending,
     Redeemed,
     DoubleSpend,
+    Unpaid,
     VoucherStore,
     memory_connect,
 )
@@ -124,6 +125,7 @@ from ..resource import (
 
 from .strategies import (
     tahoe_configs,
+    client_unpaidredeemer_configurations,
     client_doublespendredeemer_configurations,
     client_dummyredeemer_configurations,
     client_nonredeemer_configurations,
@@ -661,6 +663,28 @@ class VoucherTests(TestCase):
             ),
         )
 
+    @given(tahoe_configs(client_unpaidredeemer_configurations()), datetimes(), vouchers())
+    def test_get_known_voucher_unpaid(self, get_config, now, voucher):
+        """
+        When a voucher is first ``PUT`` and then later a ``GET`` is issued for the
+        same voucher then the response code is **OK** and details, including
+        those relevant to a voucher which has failed redemption because it has
+        not been paid for yet, about the voucher are included in a
+        json-encoded response body.
+        """
+        return self._test_get_known_voucher(
+            get_config,
+            now,
+            voucher,
+            MatchesStructure(
+                number=Equals(voucher),
+                created=Equals(now),
+                state=Equals(Unpaid(
+                    finished=now,
+                )),
+            ),
+        )
+
     def _test_get_known_voucher(self, get_config, now, voucher, voucher_matcher):
         """
         Assert that a voucher that is ``PUT`` and then ``GET`` is represented in
diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py
index 144999c137e15bc358955627dde554f028ab4bdf..7414c67103fa6cd31c361c63a32947d8615e8984 100644
--- a/src/_zkapauthorizer/tests/test_model.py
+++ b/src/_zkapauthorizer/tests/test_model.py
@@ -394,6 +394,9 @@ class UnblindedTokenStoreTests(TestCase):
             self.fail("mark_voucher_double_spent didn't raise, returned: {}".format(result))
 
 
+    # TODO: Other error states and transient states
+
+
 def store_for_test(testcase, get_config, get_now):
     """
     Create a ``VoucherStore`` in a temporary directory associated with the