diff --git a/src/PaymentServer/Persistence.hs b/src/PaymentServer/Persistence.hs
index fbb9720caf7392115ba36dff1e39249424f01a3d..e0b5a0e71b2ddd04e6c81da0702ddcfe9f67f1fb 100644
--- a/src/PaymentServer/Persistence.hs
+++ b/src/PaymentServer/Persistence.hs
@@ -1,12 +1,14 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE TypeSynonymInstances #-}
+{-# LANGUAGE NamedFieldPuns #-}
 
 module PaymentServer.Persistence
   ( Voucher
   , Fingerprint
-  , RedeemError(NotPaid, AlreadyRedeemed)
+  , RedeemError(NotPaid, AlreadyRedeemed, DuplicateFingerprint)
   , PaymentError(AlreadyPaid, PaymentFailed)
-  , VoucherDatabase(payForVoucher, redeemVoucher)
+  , VoucherDatabase(payForVoucher, redeemVoucherWithCounter)
+  , redeemVoucher
   , VoucherDatabaseState(MemoryDB, SQLiteDB)
   , memory
   , sqlite
@@ -61,6 +63,8 @@ data RedeemError =
   NotPaid
   -- | The voucher has already been redeemed.
   | AlreadyRedeemed
+  -- | The fingerprint given has already been seen.
+  | DuplicateFingerprint
   deriving (Show, Eq)
 
 -- | A fingerprint cryptographically identifies a redemption of a voucher.
@@ -74,6 +78,8 @@ data RedeemError =
 -- to support this case.
 type Fingerprint = Text
 
+type RedemptionKey = (Voucher, Integer)
+
 -- | A VoucherDatabase provides persistence for state related to vouchers.
 class VoucherDatabase d where
   -- | Change the state of the given voucher to indicate that it has been paid.
@@ -96,6 +102,18 @@ class VoucherDatabase d where
     -> Voucher                    -- ^ A voucher to consider for redemption
     -> Fingerprint                -- ^ The retry-enabling fingerprint for this redemption
     -> IO (Either RedeemError ()) -- ^ Left indicating the redemption is not allowed or Right indicating it is.
+  redeemVoucher d v f = redeemVoucherWithCounter d v f 0
+
+  -- | Attempt to redeem a voucher.  If it has not been redeemed before or it
+  -- has been redeemed with the same counter and fingerprint, the redemption
+  -- succeeds.  Otherwise, it fails.
+  redeemVoucherWithCounter
+    :: d                          -- ^ The database
+    -> Voucher                    -- ^ A voucher to consider for redemption
+    -> Fingerprint                -- ^ The retry-enabling fingerprint for this redemption
+    -> Integer                    -- ^ The counter for this redemption
+    -> IO (Either RedeemError ()) -- ^ Left indicating the redemption is not allowed or Right indicating it is.
+
 
 -- | VoucherDatabaseState is a type that captures whether we are using an
 -- in-memory voucher database that only persists state in-memory or
@@ -107,9 +125,12 @@ data VoucherDatabaseState =
   MemoryDB {
     -- | A set of vouchers which have been paid for.
     paid :: IORef (Set.Set Voucher)
-    -- | A mapping from redeemed vouchers to fingerprints associated with the
-    -- redemption.
-  , redeemed :: IORef (Map.Map Voucher Fingerprint)
+    -- | A mapping from redeemed (voucher, counter) pairs to fingerprints
+    -- associated with the redemption.
+  , redeemed :: IORef (Map.Map RedemptionKey Fingerprint)
+    -- | A map from fingerprints to redemption details for successful
+    -- redemptions.
+  , fingerprints :: IORef (Map.Map Fingerprint RedemptionKey)
   }
   | SQLiteDB { connect :: IO Sqlite.Connection }
 
@@ -131,69 +152,122 @@ instance VoucherDatabase VoucherDatabaseState where
     bracket connect Sqlite.close $ \conn ->
     insertVoucher conn voucher pay
 
-  redeemVoucher MemoryDB{ paid = paid, redeemed = redeemed } voucher fingerprint = do
-    unpaid <- Set.notMember voucher <$> readIORef paid
-    existingFingerprint <- Map.lookup voucher <$> readIORef redeemed
-    let insertFn = (modifyIORef redeemed .) . Map.insert
-    redeemVoucherHelper unpaid existingFingerprint voucher fingerprint insertFn
+  redeemVoucherWithCounter MemoryDB{ paid, redeemed, fingerprints } voucher fingerprint counter =
+    let
+      isVoucherPaid paid voucher = Set.member voucher <$> readIORef paid
+      lookupFingerprint redeemed key = Map.lookup key <$> readIORef redeemed
+      lookupVoucherCounter fingerprints fingerprint =
+        Map.lookup fingerprint <$> readIORef fingerprints
+      markVoucherRedeemed redeemed fingerprints key fingerprint = do
+        modifyIORef redeemed $ Map.insert key fingerprint
+        modifyIORef fingerprints $ Map.insert fingerprint key
+    in
+      redeemVoucherHelper
+      (isVoucherPaid paid)
+      (lookupFingerprint redeemed)
+      (lookupVoucherCounter fingerprints)
+      (markVoucherRedeemed redeemed fingerprints)
+      voucher
+      counter
+      fingerprint
 
-  redeemVoucher SQLiteDB { connect = connect } voucher fingerprint =
+  redeemVoucherWithCounter SQLiteDB { connect = connect } voucher fingerprint counter =
     bracket connect Sqlite.close $ \conn ->
     Sqlite.withExclusiveTransaction conn $
-    do
-      unpaid <- isVoucherUnpaid conn voucher
-      existingFingerprint <- getVoucherFingerprint conn voucher
-      let insertFn = insertVoucherAndFingerprint conn
-      redeemVoucherHelper unpaid existingFingerprint voucher fingerprint insertFn
+    redeemVoucherHelper
+    (isVoucherPaid conn)
+    (getVoucherFingerprint conn)
+    (getVoucherCounterForFingerprint conn)
+    (insertVoucherAndFingerprint conn)
+    voucher
+    counter
+    fingerprint
+
+
+-- | Look up the voucher, counter tuple which previously performed a
+-- redemption using the given fingerprint.
+getVoucherCounterForFingerprint :: Sqlite.Connection -> Fingerprint -> IO (Maybe RedemptionKey)
+getVoucherCounterForFingerprint dbConn fingerprint =
+  let
+    sql = "SELECT vouchers.name, redeemed.counter \
+          \FROM vouchers \
+          \INNER JOIN redeemed \
+          \ON vouchers.id = redeemed.voucher_id \
+          \AND redeemed.fingerprint = ?"
+  in
+    listToMaybe <$> Sqlite.query dbConn sql (Sqlite.Only fingerprint)
+
 
 -- | Allow a voucher to be redeemed if it has been paid for and not redeemed
 -- before or redeemed with the same fingerprint.
 redeemVoucherHelper
-  :: Bool                                -- ^ Has the voucher been paid for?
-  -> Maybe Fingerprint                   -- ^ If it has been redeemed before,
-                                         -- with what fingerprint?
-  -> Voucher                             -- ^ The voucher in question.
-  -> Fingerprint                         -- ^ The fingerprint associated with
-                                         -- the new redemption attempt.
-  -> (Voucher -> Fingerprint -> IO ())   -- ^ A function to mark the voucher
-                                         -- as redeemed if this redemption
-                                         -- should succeed.
-  -> IO (Either RedeemError ())          -- ^ Right for successful redemption,
-                                         -- left with details about why it
-                                         -- failed.
-redeemVoucherHelper unpaid existingFingerprint voucher fingerprint insertFn =
-  case (unpaid, existingFingerprint) of
-    (True, _) ->
-      return $ Left NotPaid
-    (False, Nothing) -> do
-      insertFn voucher fingerprint
-      return $ Right ()
-    (False, Just fingerprint') ->
-      if fingerprint == fingerprint' then
-        return $ Right ()
-      else
-        return $ Left AlreadyRedeemed
+  -- | Has the given voucher been paid for?
+  :: (Voucher -> IO Bool)
+  -- | If it has been redeemed, with what fingerprint?
+  -> (RedemptionKey -> IO (Maybe Fingerprint))
+  -- | What redemption attempt has the given fingerprint been used with
+  -- before, if any?
+  -> (Fingerprint -> IO (Maybe RedemptionKey))
+  -- | Mark the redemption as successful.
+  -> (RedemptionKey -> Fingerprint -> IO ())
+  -- | The voucher being used in this attempt.
+  -> Voucher
+  -- | The counter being used in this account.
+  -> Integer
+  -- | The fingerprint of the this attempt.
+  -> Fingerprint
+  -- | Right for successful redemption, left with details about why it failed.
+  -> IO (Either RedeemError ())
+redeemVoucherHelper isVoucherPaid lookupFingerprint lookupVoucherCounter markVoucherRedeemed voucher counter fingerprint = do
+  paid <- isVoucherPaid voucher
+  priorUse <- lookupVoucherCounter fingerprint
+  if (priorUse /= Just (voucher, counter)) && (priorUse /= Nothing)
+    then return $ Left DuplicateFingerprint
+    else
+    do
+      existingFingerprint <- lookupFingerprint (voucher, counter)
+      case (paid, existingFingerprint) of
+        (False, _) ->
+          return $ Left NotPaid
+        (True, Nothing) -> do
+          markVoucherRedeemed (voucher, counter) fingerprint
+          return $ Right ()
+        (True, Just fingerprint') ->
+          if fingerprint == fingerprint' then
+            return $ Right ()
+          else
+            return $ Left AlreadyRedeemed
+
 
 -- | Create a new, empty MemoryVoucherDatabase.
 memory :: IO VoucherDatabaseState
 memory = do
   paid <- newIORef mempty
   redeemed <- newIORef mempty
-  return $ MemoryDB paid redeemed
+  fingerprints <- newIORef mempty
+  return $ MemoryDB paid redeemed fingerprints
 
 instance FromRow Fingerprint where
   fromRow = Sqlite.field
 
--- | Checks if the given `voucher` is unpaid.
-isVoucherUnpaid :: Sqlite.Connection -> Voucher -> IO Bool
-isVoucherUnpaid dbConn voucher =
-  null <$> (Sqlite.query dbConn "SELECT 1 FROM vouchers WHERE vouchers.name = ? LIMIT 1" (Sqlite.Only voucher) :: IO [Sqlite.Only Int])
+-- | Checks if the given `voucher` is paid.
+isVoucherPaid :: Sqlite.Connection -> Voucher -> IO Bool
+isVoucherPaid dbConn voucher =
+  not . null <$> (Sqlite.query dbConn "SELECT 1 FROM vouchers WHERE vouchers.name = ? LIMIT 1" (Sqlite.Only voucher) :: IO [Sqlite.Only Int])
 
--- | Retrieve an existing redemption fingerprint for the given voucher, if
--- there is one.
-getVoucherFingerprint :: Sqlite.Connection -> Voucher -> IO (Maybe Fingerprint)
-getVoucherFingerprint dbConn voucher =
-  listToMaybe <$> Sqlite.query dbConn "SELECT redeemed.fingerprint FROM vouchers INNER JOIN redeemed ON vouchers.id = redeemed.voucher_id AND vouchers.name = ?" (Sqlite.Only voucher)
+-- | Retrieve an existing redemption fingerprint for the given voucher and
+-- counter, if there is one.
+getVoucherFingerprint :: Sqlite.Connection -> RedemptionKey -> IO (Maybe Fingerprint)
+getVoucherFingerprint dbConn (voucher, counter) =
+  let
+    sql = "SELECT redeemed.fingerprint \
+          \FROM vouchers \
+          \INNER JOIN redeemed \
+          \ON vouchers.id = redeemed.voucher_id \
+          \AND vouchers.name = ? \
+          \AND redeemed.counter = ?"
+  in
+    listToMaybe <$> Sqlite.query dbConn sql ((voucher :: Text), (counter :: Integer))
 
 -- | Mark the given voucher as paid in the database.
 insertVoucher :: Sqlite.Connection -> Voucher -> IO a -> IO a
@@ -225,9 +299,13 @@ insertVoucher dbConn voucher pay =
 
 -- | Mark the given voucher as having been redeemed (with the given
 -- fingerprint) in the database.
-insertVoucherAndFingerprint :: Sqlite.Connection -> Voucher -> Fingerprint -> IO ()
-insertVoucherAndFingerprint dbConn voucher fingerprint =
-  Sqlite.execute dbConn "INSERT INTO redeemed (voucher_id, fingerprint) VALUES ((SELECT id FROM vouchers WHERE name = ?), ?)" (voucher, fingerprint)
+insertVoucherAndFingerprint :: Sqlite.Connection -> RedemptionKey -> Fingerprint -> IO ()
+insertVoucherAndFingerprint dbConn (voucher, counter) fingerprint =
+  let
+    sql = "INSERT INTO redeemed (voucher_id, counter, fingerprint) \
+          \VALUES ((SELECT id FROM vouchers WHERE name = ?), ?, ?)"
+  in
+    Sqlite.execute dbConn sql (voucher, counter, fingerprint)
 
 -- | Open and create (if necessary) a SQLite3 database which can persistently
 -- store all of the relevant information about voucher state.
@@ -240,7 +318,7 @@ sqlite path =
       let exec = Sqlite.execute_ dbConn
       exec "PRAGMA foreign_keys = ON"
       exec "CREATE TABLE IF NOT EXISTS vouchers (id INTEGER PRIMARY KEY, name TEXT UNIQUE)"
-      exec "CREATE TABLE IF NOT EXISTS redeemed (id INTEGER PRIMARY KEY, voucher_id INTEGER, fingerprint TEXT, FOREIGN KEY (voucher_id) REFERENCES vouchers(id))"
+      exec "CREATE TABLE IF NOT EXISTS redeemed (id INTEGER PRIMARY KEY, voucher_id INTEGER, counter INTEGER, fingerprint TEXT, FOREIGN KEY (voucher_id) REFERENCES vouchers(id))"
       return dbConn
   in
     return . SQLiteDB $ connect
diff --git a/src/PaymentServer/Redemption.hs b/src/PaymentServer/Redemption.hs
index 33d488f18e3c288d95f082f8e85d1a926e640bbb..701677d2e97142aabdc7f265569b47c2f0b2cba6 100644
--- a/src/PaymentServer/Redemption.hs
+++ b/src/PaymentServer/Redemption.hs
@@ -55,8 +55,8 @@ import Crypto.Hash
   , hashWith
   )
 import PaymentServer.Persistence
-  ( VoucherDatabase(redeemVoucher)
-  , RedeemError(NotPaid, AlreadyRedeemed)
+  ( VoucherDatabase(redeemVoucherWithCounter)
+  , RedeemError(NotPaid, AlreadyRedeemed, DuplicateFingerprint)
   , Fingerprint
   , Voucher
   )
@@ -164,12 +164,14 @@ redeem issue database (Redeem voucher tokens counter) =
     throwError $ jsonErr400 (CounterOutOfBounds 0 maxCounter counter)
   else do
     let fingerprint = fingerprintFromTokens tokens
-    result <- liftIO $ PaymentServer.Persistence.redeemVoucher database voucher fingerprint
+    result <- liftIO $ redeemVoucherWithCounter database voucher fingerprint counter
     case result of
       Left NotPaid -> do
         throwError $ jsonErr400 Unpaid
       Left AlreadyRedeemed -> do
         throwError $ jsonErr400 DoubleSpend
+      Left DuplicateFingerprint -> do
+        throwError $ jsonErr400 $ OtherFailure "fingerprint already used"
       Right () -> do
         let result = issue tokens
         case result of
diff --git a/test/Persistence.hs b/test/Persistence.hs
index a66196f2433aed44fdf9bea7980c0337735e6ebc..7c0f97582c983e8157edd8a5198607ad31800bbb 100644
--- a/test/Persistence.hs
+++ b/test/Persistence.hs
@@ -39,9 +39,9 @@ import System.Directory
 import PaymentServer.Persistence
   ( Voucher
   , Fingerprint
-  , RedeemError(NotPaid, AlreadyRedeemed)
+  , RedeemError(NotPaid, AlreadyRedeemed, DuplicateFingerprint)
   , PaymentError(AlreadyPaid)
-  , VoucherDatabase(payForVoucher, redeemVoucher)
+  , VoucherDatabase(payForVoucher, redeemVoucher, redeemVoucherWithCounter)
   , memory
   , sqlite
   )
@@ -61,6 +61,7 @@ tests = testGroup "Persistence"
 voucher = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
 anotherVoucher = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
 fingerprint = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+anotherFingerprint = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
 
 -- Mock a successful payment.
 paySuccessfully :: IO ()
@@ -109,6 +110,24 @@ makeVoucherPaymentTests label makeDatabase =
       second <- redeem (Text.cons 'a' $ Text.tail fingerprint)
       assertEqual "redeeming paid voucher" (Right ()) first
       assertEqual "re-redeeming paid voucher" (Left AlreadyRedeemed) second
+  , testCase "allowed redemption varying by counter" $ do
+      connect <- makeDatabase
+      conn <- connect
+      () <- payForVoucher conn voucher paySuccessfully
+      let redeem = redeemVoucherWithCounter conn voucher
+      first <- redeem fingerprint 0
+      second <- redeem anotherFingerprint 1
+      assertEqual "redeemed with counter 0" (Right ()) first
+      assertEqual "redeemed with counter 1" (Right ()) second
+  , testCase "disallowed redemption varying by counter but not fingerprint" $ do
+      connect <- makeDatabase
+      conn <- connect
+      () <- payForVoucher conn voucher paySuccessfully
+      let redeem = redeemVoucherWithCounter conn voucher
+      first <- redeem fingerprint 0
+      second <- redeem fingerprint 1
+      assertEqual "redeemed with counter 0" (Right ()) first
+      assertEqual "redeemed with counter 1" (Left DuplicateFingerprint) second
   , testCase "pay with exception" $ do
       connect <- makeDatabase
       conn <- connect
@@ -150,7 +169,7 @@ makeVoucherPaymentTests label makeDatabase =
       -- It does matter which connection is used to redeem the voucher.  A
       -- connection can only do one thing at a time.
       let redeem = redeemVoucher connA voucher fingerprint
-      let anotherRedeem = redeemVoucher connB anotherVoucher fingerprint
+      let anotherRedeem = redeemVoucher connB anotherVoucher anotherFingerprint
 
       result <- withAsync redeem $ \r1 -> do
         withAsync anotherRedeem $ \r2 -> do