diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py
index 58357cb237a36e9d1ac300529ccb0d9bdad0c03b..ec534fd4c4801cffc072c6af5f7149bd7724a253 100644
--- a/src/_zkapauthorizer/model.py
+++ b/src/_zkapauthorizer/model.py
@@ -117,6 +117,15 @@ def open_and_initialize(path, required_schema_version, connect=None):
             )
             """,
         )
+        cursor.execute(
+            """
+            CREATE TABLE IF NOT EXISTS [passes] (
+                [text] text, -- The string that defines the pass.
+
+                PRIMARY KEY([text])
+            )
+            """,
+        )
     return conn
 
 
@@ -201,6 +210,76 @@ class VoucherStore(object):
             in refs
         )
 
+    @with_cursor
+    def insert_passes(self, cursor, passes):
+        """
+        Store some passes.
+
+        :param list[Pass] passes: The passes to store.
+        """
+        cursor.executemany(
+            """
+            INSERT INTO [passes] VALUES (?)
+            """,
+            list((p.text,) for p in passes),
+        )
+
+    @with_cursor
+    def extract_passes(self, cursor, count):
+        """
+        Remove and return some passes.
+
+        :param int count: The maximum number of passes to remove and return.
+            If fewer passes than this are available, only as many as are
+            available are returned.
+
+        :return list[Pass]: The removed passes.
+        """
+        cursor.execute(
+            """
+            CREATE TEMPORARY TABLE [extracting-passes]
+            AS
+            SELECT [text] FROM [passes] LIMIT ?
+            """,
+            (count,),
+        )
+        cursor.execute(
+            """
+            DELETE FROM [passes] WHERE [text] IN [extracting-passes]
+            """,
+        )
+        cursor.execute(
+            """
+            SELECT ([text]) FROM [extracting-passes]
+            """,
+        )
+        texts = cursor.fetchall()
+        cursor.execute(
+            """
+            DROP TABLE [extracting-passes]
+            """,
+        )
+        return list(
+            Pass(t)
+            for (t,)
+            in texts
+        )
+
+
+@attr.s(frozen=True)
+class Pass(object):
+    """
+    A ``Pass`` instance completely represents a single Zero-Knowledge Access Pass.
+
+    :ivar unicode text: The text value of the pass.  This can be sent to a
+        service provider one time to anonymously prove a prior voucher
+        redemption.  If it is sent more than once the service provider may
+        choose to reject it and the anonymity property is compromised.  Pass
+        text should be kept secret.  If pass text is divulged to third-parties
+        the anonymity property may be compromised.
+    """
+    text = attr.ib(type=unicode)
+
 
 @attr.s
 class Voucher(object):
diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py
index e615137fd615201776e7ccfa46ba1d908a8f883b..3b6fc63deeab705795a5d7555b5a453dd6e16035 100644
--- a/src/_zkapauthorizer/tests/strategies.py
+++ b/src/_zkapauthorizer/tests/strategies.py
@@ -52,6 +52,10 @@ from allmydata.client import (
     config_from_string,
 )
 
+from ..model import (
+    Pass,
+)
+
 
 def _merge_dictionaries(dictionaries):
     result = {}
@@ -175,6 +179,20 @@ def vouchers():
     )
 
 
+def zkaps():
+    """
+    Build random ZKAPs as ``Pass` instances.
+    """
+    return binary(
+        min_size=32,
+        max_size=32,
+    ).map(
+        urlsafe_b64encode,
+    ).map(
+        lambda zkap: Pass(zkap.decode("ascii")),
+    )
+
+
 def request_paths():
     """
     Build lists of unicode strings that represent the path component of an
diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py
index 7732e5e06f40460bf1acf00665ab1e4501298afa..11d730440ebb90a09a2a9559766f9fe90442bb1d 100644
--- a/src/_zkapauthorizer/tests/test_model.py
+++ b/src/_zkapauthorizer/tests/test_model.py
@@ -64,6 +64,7 @@ from ..model import (
 from .strategies import (
     tahoe_configs,
     vouchers,
+    zkaps,
 )
 
 
@@ -146,7 +147,7 @@ class VoucherStoreTests(TestCase):
         )
 
 
-    @given(tahoe_configs(), lists(vouchers()))
+    @given(tahoe_configs(), lists(vouchers(), unique=True))
     def test_list(self, get_config, vouchers):
         """
         ``VoucherStore.list`` returns a ``list`` containing a ``Voucher`` object
@@ -250,3 +251,27 @@ class VoucherTests(TestCase):
             Voucher.from_json(ref.to_json()),
             Equals(ref),
         )
+
+
+class ZKAPStoreTests(TestCase):
+    """
+    Tests for ZKAP-related functionality of ``VoucherStore``.
+    """
+    @given(tahoe_configs(), lists(zkaps(), unique=True))
+    def test_zkaps_round_trip(self, get_config, passes):
+        """
+        ZKAPs that are added to the store can later be retrieved.
+        """
+        tempdir = self.useFixture(TempDir())
+        config = get_config(tempdir.join(b"node"), b"tub.port")
+        store = VoucherStore.from_node_config(
+            config,
+            memory_connect,
+        )
+        store.insert_passes(passes)
+        retrieved_passes = store.extract_passes(len(passes))
+        self.expectThat(passes, Equals(retrieved_passes))
+
+        # After extraction, the passes are no longer available.
+        more_passes = store.extract_passes(1)
+        self.expectThat([], Equals(more_passes))