diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py
index 5768b34bf8e9bf3e26aef80c027c6d589bf64e3d..142d626e40f928d78130a993c9b4949dcbd5ef1f 100644
--- a/src/_zkapauthorizer/controller.py
+++ b/src/_zkapauthorizer/controller.py
@@ -581,9 +581,9 @@ class PaymentController(object):
         redeeming a voucher, if no other count is given when the redemption is
         started.
 
-    :ivar dict[unicode, datetime] _active: A mapping from voucher identifiers
-        which currently have redemption attempts in progress to timestamps
-        when the attempt began.
+    :ivar dict[unicode, Redeeming] _active: A mapping from voucher identifiers
+        which currently have redemption attempts in progress to a
+        ``Redeeming`` state representing the attempt.
 
     :ivar dict[unicode, datetime] _error: A mapping from voucher identifiers
         which have recently failed with an unrecognized, transient error.
@@ -664,18 +664,32 @@ class PaymentController(object):
         This will not persist the voucher or random tokens but it will persist
         the result.
         """
+        if not isinstance(voucher.state, model_Pending):
+            raise ValueError(
+                "Cannot redeem voucher in state {} instead of Pending.".format(
+                    voucher.state,
+                ),
+            )
+
         # Ask the redeemer to do the real task of redemption.
         self._log.info("Redeeming random tokens for a voucher ({voucher}).", voucher=voucher)
         d = bracket(
-            lambda: setitem(self._active, voucher, self.store.now()),
-            lambda: delitem(self._active, voucher),
-            lambda: self.redeemer.redeemWithCounter(Voucher(voucher), counter, random_tokens),
+            lambda: setitem(
+                self._active,
+                voucher.number,
+                model_Redeeming(
+                    started=self.store.now(),
+                    counter=voucher.state.counter,
+                ),
+            ),
+            lambda: delitem(self._active, voucher.number),
+            lambda: self.redeemer.redeemWithCounter(voucher.number, counter, random_tokens),
         )
         d.addCallbacks(
-            partial(self._redeemSuccess, voucher),
-            partial(self._redeemFailure, voucher),
+            partial(self._redeemSuccess, voucher.number),
+            partial(self._redeemFailure, voucher.number),
         )
-        d.addErrback(partial(self._finalRedeemError, voucher))
+        d.addErrback(partial(self._finalRedeemError, voucher.number))
         return d
 
     def _get_random_tokens_for_voucher(self, voucher, num_tokens):
@@ -713,7 +727,7 @@ class PaymentController(object):
         # we should skip any already-redeemed counter values.
         #
         # https://github.com/PrivateStorageio/ZKAPAuthorizer/issues/124
-        return self._perform_redeem(voucher, 0, tokens)
+        return self._perform_redeem(self.store.get(voucher), 0, tokens)
 
     def _redeemSuccess(self, voucher, result):
         """
@@ -776,7 +790,7 @@ class PaymentController(object):
             if voucher.number in self._active:
                 return attr.evolve(
                     voucher,
-                    state=model_Redeeming(started=self._active[voucher.number]),
+                    state=self._active[voucher.number],
                 )
             if voucher.number in self._unpaid:
                 return attr.evolve(
diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py
index f8d69c2ca21b2cc6061f2d86673e30a1bd2a68be..fed05e1d489b12864512628bb60e01b0173b25b9 100644
--- a/src/_zkapauthorizer/model.py
+++ b/src/_zkapauthorizer/model.py
@@ -722,6 +722,15 @@ class RandomToken(object):
     )
 
 
+def _counter_attribute():
+    return attr.ib(
+        validator=attr.validators.and_(
+            attr.validators.instance_of((int, long)),
+            greater_than(-1),
+        ),
+    )
+
+
 @attr.s(frozen=True)
 class Pending(object):
     """
@@ -730,12 +739,7 @@ class Pending(object):
     :ivar int counter: The number of partial redemptions which have been
         successfully performed for the voucher.
     """
-    counter = attr.ib(
-        validator=attr.validators.and_(
-            attr.validators.instance_of((int, long)),
-            greater_than(-1),
-        ),
-    )
+    counter = _counter_attribute()
 
     def should_start_redemption(self):
         return True
@@ -755,6 +759,7 @@ class Redeeming(object):
     progress.
     """
     started = attr.ib(validator=attr.validators.instance_of(datetime))
+    counter = _counter_attribute()
 
     def should_start_redemption(self):
         return False
@@ -849,7 +854,7 @@ class Error(object):
         }
 
 
-@attr.s
+@attr.s(frozen=True)
 class Voucher(object):
     """
     :ivar unicode number: The text string which gives this voucher its
@@ -873,7 +878,17 @@ class Voucher(object):
         default=None,
         validator=attr.validators.optional(attr.validators.instance_of(datetime)),
     )
-    state = attr.ib(default=Pending(counter=0))
+    state = attr.ib(
+        default=Pending(counter=0),
+        validator=attr.validators.instance_of((
+            Pending,
+            Redeeming,
+            Redeemed,
+            DoubleSpend,
+            Unpaid,
+            Error,
+        )),
+    )
 
     @classmethod
     def from_row(cls, row):
@@ -922,6 +937,7 @@ class Voucher(object):
         elif state_name == u"redeeming":
             state = Redeeming(
                 started=parse_datetime(state_json[u"started"]),
+                counter=0,
             )
         elif state_name == u"double-spend":
             state = DoubleSpend(
diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py
index f6d2abde71ebfdb238ce37477f4f0af4f657e926..6c2089a999b53ae1a655b824de86424144c28752 100644
--- a/src/_zkapauthorizer/tests/test_client_resource.py
+++ b/src/_zkapauthorizer/tests/test_client_resource.py
@@ -795,6 +795,7 @@ class VoucherTests(TestCase):
                 created=Equals(now),
                 state=Equals(Redeeming(
                     started=now,
+                    counter=0,
                 )),
             ),
         )
diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py
index 16d12554bfc9aececbf627543c14c8102c2e1923..9d97ce8b75054deac17e99272ad30ddbeba4996d 100644
--- a/src/_zkapauthorizer/tests/test_controller.py
+++ b/src/_zkapauthorizer/tests/test_controller.py
@@ -112,6 +112,7 @@ from ..controller import (
 from ..model import (
     UnblindedToken,
     Pending as model_Pending,
+    Redeeming as model_Redeeming,
     DoubleSpend as model_DoubleSpend,
     Redeemed as model_Redeemed,
     Unpaid as model_Unpaid,
@@ -156,6 +157,29 @@ class PaymentControllerTests(TestCase):
             Equals(model_Pending(counter=0)),
         )
 
+    @given(tahoe_configs(), datetimes(), vouchers())
+    def test_redeeming(self, get_config, now, voucher):
+        """
+        A ``Voucher`` is marked redeeming while ``IRedeemer.redeem`` is actively
+        working on redeeming it.
+        """
+        store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
+        controller = PaymentController(
+            store,
+            NonRedeemer(),
+            default_token_count=10,
+        )
+        controller.redeem(voucher)
+
+        controller_voucher = controller.get_voucher(voucher)
+        self.assertThat(
+            controller_voucher.state,
+            Equals(model_Redeeming(
+                started=now,
+                counter=0,
+            )),
+        )
+
     @given(tahoe_configs(), dummy_ristretto_keys(), datetimes(), vouchers())
     def test_redeemed_after_redeeming(self, get_config, public_key, now, voucher):
         """