diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py
index 142d626e40f928d78130a993c9b4949dcbd5ef1f..b438680517bfbde7c76cf4f2b031e5c1ef5b6112 100644
--- a/src/_zkapauthorizer/controller.py
+++ b/src/_zkapauthorizer/controller.py
@@ -696,11 +696,18 @@ class PaymentController(object):
         """
         Generate or load random tokens for a redemption attempt of a voucher.
         """
+        counter = 0
         def get_tokens():
-            self._log.info("Generating random tokens for a voucher ({voucher}).", voucher=voucher)
-            return self.redeemer.random_tokens_for_voucher(Voucher(voucher), num_tokens)
+            self._log.info(
+                "Generating random tokens for a voucher ({voucher}).",
+                voucher=voucher,
+            )
+            return self.redeemer.random_tokens_for_voucher(
+                Voucher(voucher),
+                num_tokens,
+            )
 
-        return self.store.add(voucher, get_tokens)
+        return self.store.add(voucher, counter, get_tokens)
 
     def redeem(self, voucher, num_tokens=None):
         """
diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py
index fed05e1d489b12864512628bb60e01b0173b25b9..0db7e74b7643d770727408fb7228eac226a539b9 100644
--- a/src/_zkapauthorizer/model.py
+++ b/src/_zkapauthorizer/model.py
@@ -225,12 +225,17 @@ class VoucherStore(object):
         return Voucher.from_row(refs[0])
 
     @with_cursor
-    def add(self, cursor, voucher, get_tokens):
+    def add(self, cursor, voucher, counter, get_tokens):
         """
-        Add a new voucher and associated random tokens to the database.  If a
-        voucher with the given text value is already present, do nothing.
+        Add random tokens associated with a voucher (possibly new, possibly
+        existing) to the database.  If the (voucher, counter) pair is already
+        present, do nothing.
 
-        :param unicode voucher: The text value of a voucher to add.
+        :param unicode voucher: The text value of a voucher with which to
+            associate the tokens.
+
+        :param int counter: The redemption counter for the given voucher with
+            which to associate the tokens.
 
         :param list[RandomToken]: The tokens to add alongside the voucher.
         """
@@ -243,16 +248,17 @@ class VoucherStore(object):
             """
             SELECT ([text])
             FROM [tokens]
-            WHERE [voucher] = ?
+            WHERE [voucher] = ? AND [counter] = ?
             """,
-            (voucher,),
+            (voucher, counter),
         )
         rows = cursor.fetchall()
         if len(rows) > 0:
             self._log.info(
-                "Loaded {count} random tokens for a voucher ({voucher}).",
+                "Loaded {count} random tokens for a voucher ({voucher}[{counter}]).",
                 count=len(rows),
                 voucher=voucher,
+                counter=counter,
             )
             tokens = list(
                 RandomToken(token_value)
@@ -262,9 +268,10 @@ class VoucherStore(object):
         else:
             tokens = get_tokens()
             self._log.info(
-                "Persisting {count} random tokens for a voucher ({voucher}).",
+                "Persisting {count} random tokens for a voucher ({voucher}[{counter}]).",
                 count=len(tokens),
                 voucher=voucher,
+                counter=counter,
             )
             cursor.execute(
                 """
@@ -274,10 +281,10 @@ class VoucherStore(object):
             )
             cursor.executemany(
                 """
-                INSERT INTO [tokens] ([voucher], [text]) VALUES (?, ?)
+                INSERT INTO [tokens] ([voucher], [counter], [text]) VALUES (?, ?, ?)
                 """,
                 list(
-                    (voucher, token.token_value)
+                    (voucher, counter, token.token_value)
                     for token
                     in tokens
                 ),
diff --git a/src/_zkapauthorizer/schema.py b/src/_zkapauthorizer/schema.py
index e3a37eb2023cfe890fb08a7500fdd519e9335bd0..4fa33b13ae1f1001816e269bfd0b49ded10951bb 100644
--- a/src/_zkapauthorizer/schema.py
+++ b/src/_zkapauthorizer/schema.py
@@ -138,4 +138,12 @@ _UPGRADES = {
         ALTER TABLE [vouchers] ADD COLUMN [counter] integer DEFAULT 0
         """,
     ],
+
+    3: [
+        """
+        -- Reference to the counter these tokens go with.
+        ALTER TABLE [tokens] ADD COLUMN [counter] integer NOT NULL DEFAULT 0
+        """,
+
+    ],
 }
diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py
index e6df76c1ad9921552af6abb0667f341d76f7c57c..3b0a1f52f86252d235b9b9520289fe7f17854a49 100644
--- a/src/_zkapauthorizer/tests/test_model.py
+++ b/src/_zkapauthorizer/tests/test_model.py
@@ -88,6 +88,7 @@ from .strategies import (
     tahoe_configs,
     vouchers,
     voucher_objects,
+    voucher_counters,
     random_tokens,
     unblinded_tokens,
     posix_safe_datetimes,
@@ -124,7 +125,7 @@ class VoucherStoreTests(TestCase):
         previously added to the store with ``VoucherStore.add``.
         """
         store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
-        store.add(voucher, lambda: tokens)
+        store.add(voucher, 0, lambda: tokens)
         self.assertThat(
             store.get(voucher),
             MatchesStructure(
@@ -134,6 +135,39 @@ class VoucherStoreTests(TestCase):
             ),
         )
 
+    @given(
+        tahoe_configs(),
+        vouchers(),
+        lists(voucher_counters(), unique=True, min_size=2, max_size=2),
+        lists(random_tokens(), min_size=2, unique=True),
+        datetimes(),
+    )
+    def test_add_with_distinct_counters(self, get_config, voucher, counters, tokens, now):
+        """
+        ``VoucherStore.add`` adds new tokens to the store when passed the same
+        voucher but a different counter value.
+        """
+        counter_a = counters[0]
+        counter_b = counters[1]
+        tokens_a = tokens[:len(tokens) / 2]
+        tokens_b = tokens[len(tokens) / 2:]
+
+        store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
+        added_tokens_a = store.add(voucher, counter_a, lambda: tokens_a)
+        added_tokens_b = store.add(voucher, counter_b, lambda: tokens_b)
+
+        self.assertThat(
+            store.get(voucher),
+            MatchesStructure(
+                number=Equals(voucher),
+                state=Equals(Pending(counter=0)),
+                created=Equals(now),
+            ),
+        )
+
+        self.assertThat(tokens_a, Equals(added_tokens_a))
+        self.assertThat(tokens_b, Equals(added_tokens_b))
+
     @given(tahoe_configs(), vouchers(), datetimes(), lists(random_tokens(), unique=True))
     def test_add_idempotent(self, get_config, voucher, now, tokens):
         """
@@ -141,8 +175,8 @@ class VoucherStoreTests(TestCase):
         in the same state as a single call.
         """
         store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
-        first_tokens = store.add(voucher, lambda: tokens)
-        second_tokens = store.add(voucher, lambda: [])
+        first_tokens = store.add(voucher, 0, lambda: tokens)
+        second_tokens = store.add(voucher, 0, lambda: [])
         self.assertThat(
             store.get(voucher),
             MatchesStructure(
@@ -168,7 +202,7 @@ class VoucherStoreTests(TestCase):
         """
         store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
         for voucher in vouchers:
-            store.add(voucher, lambda: [])
+            store.add(voucher, 0, lambda: [])
 
         self.assertThat(
             store.list(),
@@ -306,7 +340,7 @@ class VoucherStoreTests(TestCase):
 
         # Put some tokens in it that we can backup and extract
         random_tokens, unblinded_tokens = paired_tokens(data, integers(min_value=1, max_value=5))
-        store.add(voucher_value, lambda: random_tokens)
+        store.add(voucher_value, 0, lambda: random_tokens)
         store.insert_unblinded_tokens_for_voucher(
             voucher_value,
             public_key,
@@ -478,7 +512,7 @@ class UnblindedTokenStoreTests(TestCase):
         """
         random_tokens, unblinded_tokens = paired_tokens(data)
         store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
-        store.add(voucher_value, lambda: random_tokens)
+        store.add(voucher_value, 0, lambda: random_tokens)
         store.insert_unblinded_tokens_for_voucher(voucher_value, public_key, unblinded_tokens)
         retrieved_tokens = store.extract_unblinded_tokens(len(random_tokens))
 
@@ -524,7 +558,7 @@ class UnblindedTokenStoreTests(TestCase):
         )
 
         store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
-        store.add(voucher_value, lambda: random)
+        store.add(voucher_value, 0, lambda: random)
         store.insert_unblinded_tokens_for_voucher(voucher_value, public_key, unblinded)
         loaded_voucher = store.get(voucher_value)
         self.assertThat(
@@ -550,7 +584,7 @@ class UnblindedTokenStoreTests(TestCase):
         such.
         """
         store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
-        store.add(voucher_value, lambda: random_tokens)
+        store.add(voucher_value, 0, lambda: random_tokens)
         store.mark_voucher_double_spent(voucher_value)
         voucher = store.get(voucher_value)
         self.assertThat(
@@ -591,7 +625,7 @@ class UnblindedTokenStoreTests(TestCase):
             ),
         )
         store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
-        store.add(voucher_value, lambda: random)
+        store.add(voucher_value, 0, lambda: random)
         store.insert_unblinded_tokens_for_voucher(voucher_value, public_key, unblinded)
         self.assertThat(
             lambda: store.mark_voucher_double_spent(voucher_value),
@@ -644,7 +678,7 @@ class UnblindedTokenStoreTests(TestCase):
             ),
         )
         store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
-        store.add(voucher_value, lambda: random)
+        store.add(voucher_value, 0, lambda: random)
         store.insert_unblinded_tokens_for_voucher(voucher_value, public_key, unblinded)
 
         self.assertThat(