diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py
index 8615b43955dd64b54a7ea0b2827bb283f6fc02e7..f8d69c2ca21b2cc6061f2d86673e30a1bd2a68be 100644
--- a/src/_zkapauthorizer/model.py
+++ b/src/_zkapauthorizer/model.py
@@ -211,7 +211,7 @@ class VoucherStore(object):
         cursor.execute(
             """
             SELECT
-                [number], [created], [state], [finished], [token-count], [public-key]
+                [number], [created], [state], [finished], [token-count], [public-key], [counter]
             FROM
                 [vouchers]
             WHERE
@@ -295,7 +295,7 @@ class VoucherStore(object):
         cursor.execute(
             """
             SELECT
-                [number], [created], [state], [finished], [token-count], [public-key]
+                [number], [created], [state], [finished], [token-count], [public-key], [counter]
             FROM
                 [vouchers]
             """,
@@ -637,6 +637,20 @@ def has_length(expected):
             )
     return validate_has_length
 
+def greater_than(expected):
+    def validate_relation(inst, attr, value):
+        if value > expected:
+            return None
+
+        raise ValueError(
+            "{name!r} must be greater than {expected}, instead it was {actual}".format(
+                name=attr.name,
+                expected=expected,
+                actual=value,
+            ),
+        )
+    return validate_relation
+
 
 @attr.s(frozen=True)
 class UnblindedToken(object):
@@ -710,12 +724,26 @@ class RandomToken(object):
 
 @attr.s(frozen=True)
 class Pending(object):
+    """
+    The voucher has not yet been completely redeemed for ZKAPs.
+
+    :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),
+        ),
+    )
+
     def should_start_redemption(self):
         return True
 
     def to_json_v1(self):
         return {
             u"name": u"pending",
+            u"counter": self.counter,
         }
 
 
@@ -845,13 +873,15 @@ class Voucher(object):
         default=None,
         validator=attr.validators.optional(attr.validators.instance_of(datetime)),
     )
-    state = attr.ib(default=Pending())
+    state = attr.ib(default=Pending(counter=0))
 
     @classmethod
     def from_row(cls, row):
         def state_from_row(state, row):
             if state == u"pending":
-                return Pending()
+                # TODO: The 0 here should be row[3] but I can't write a test
+                # to prove it yet.
+                return Pending(counter=0)
             if state == u"double-spend":
                 return DoubleSpend(
                     parse_datetime(row[0], delimiter=u" "),
@@ -888,7 +918,7 @@ class Voucher(object):
         state_json = values[u"state"]
         state_name = state_json[u"name"]
         if state_name == u"pending":
-            state = Pending()
+            state = Pending(counter=state_json[u"counter"])
         elif state_name == u"redeeming":
             state = Redeeming(
                 started=parse_datetime(state_json[u"started"]),
diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py
index b648c5c5778e9d7d3d44572ecf5f05657e85c228..7776ff739b3ec50b88b37f8ecf1a307a7eba1318 100644
--- a/src/_zkapauthorizer/tests/strategies.py
+++ b/src/_zkapauthorizer/tests/strategies.py
@@ -310,10 +310,13 @@ def redeemed_states():
 
 def voucher_states():
     """
-    Build unicode strings giving states a Voucher can be in.
+    Build Python objects representing states a Voucher can be in.
     """
     return one_of(
-        just(Pending()),
+        builds(
+            Pending,
+            integers(min_value=0),
+        ),
         redeemed_states(),
         builds(
             DoubleSpend,
diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py
index 608ef72679232b29a3681b224d2eb61f98f2fe2e..16d12554bfc9aececbf627543c14c8102c2e1923 100644
--- a/src/_zkapauthorizer/tests/test_controller.py
+++ b/src/_zkapauthorizer/tests/test_controller.py
@@ -153,7 +153,7 @@ class PaymentControllerTests(TestCase):
         persisted_voucher = store.get(voucher)
         self.assertThat(
             persisted_voucher.state,
-            Equals(model_Pending()),
+            Equals(model_Pending(counter=0)),
         )
 
     @given(tahoe_configs(), dummy_ristretto_keys(), datetimes(), vouchers())
diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py
index 27fbcab25f7d9597c28ee61372c080c86df6cf07..e6df76c1ad9921552af6abb0667f341d76f7c57c 100644
--- a/src/_zkapauthorizer/tests/test_model.py
+++ b/src/_zkapauthorizer/tests/test_model.py
@@ -129,7 +129,7 @@ class VoucherStoreTests(TestCase):
             store.get(voucher),
             MatchesStructure(
                 number=Equals(voucher),
-                state=Equals(Pending()),
+                state=Equals(Pending(counter=0)),
                 created=Equals(now),
             ),
         )
@@ -148,7 +148,7 @@ class VoucherStoreTests(TestCase):
             MatchesStructure(
                 number=Equals(voucher),
                 created=Equals(now),
-                state=Equals(Pending()),
+                state=Equals(Pending(counter=0)),
             ),
         )
         self.assertThat(