diff --git a/haskoin-store-data.cabal b/haskoin-store-data.cabal
--- a/haskoin-store-data.cabal
+++ b/haskoin-store-data.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           haskoin-store-data
-version:        1.8.0
+version:        1.8.1
 synopsis:       Data for Haskoin Store
 description:    Please see the README on GitHub at <https://github.com/jprupp/haskoin-store#readme>
 category:       Bitcoin, Finance, Network
diff --git a/src/Haskoin/Store/Data.hs b/src/Haskoin/Store/Data.hs
--- a/src/Haskoin/Store/Data.hs
+++ b/src/Haskoin/Store/Data.hs
@@ -13,6 +13,35 @@
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE NoFieldSelectors #-}
 
+-- |
+-- Module      : Haskoin.Store.Data
+-- Copyright   : (c) Haskoin Developers
+-- License     : MIT
+-- Maintainer  : jprupp@protonmail.ch
+-- Stability   : experimental
+-- Portability : POSIX
+--
+-- Data types of Haskoin Store: what it stores in its database and what its API
+-- returns.
+--
+-- Most types have three encodings:
+--
+-- * binary ('Serial' and 'Binary'/'Serialize' instances), used in the database
+--   and in binary API responses;
+-- * JSON ('ToJSON' and 'FromJSON' instances), used in JSON API responses;
+-- * for types that contain addresses, JSON functions that take the 'Network'
+--   (the @marshal@ functions of 'MarshalJSON'), since the text form of an
+--   address depends on the network.
+--
+-- Types whose names start with @Binfo@ belong to the API compatible with
+-- Blockchain.info.
+-- These types live in their own package, @haskoin-store-data@, so that clients of
+-- the API can use them without depending on RocksDB, Redis or the server.
+--
+-- Values that refer to positions in the chain sort newest first: the binary
+-- encoding of 'BlockRef' stores inverted heights and times, and mempool positions
+-- sort before confirmed ones. That is the order in which wallets and explorers
+-- want history, so database scans return it directly.
 module Haskoin.Store.Data
   ( -- * Address Balances
     Balance (..),
@@ -185,18 +214,25 @@
 import Haskoin
 import Web.Scotty.Trans (Parsable (..))
 
+-- | Type of the addresses derived from an extended public key.
 data DeriveType
-  = DeriveNormal
-  | DeriveP2SH
-  | DeriveP2WPKH
+  = -- | legacy pay-to-public-key-hash addresses (@normal@)
+    DeriveNormal
+  | -- | pay-to-witness-public-key-hash nested in pay-to-script-hash
+    -- (@compat@)
+    DeriveP2SH
+  | -- | native pay-to-witness-public-key-hash (@segwit@)
+    DeriveP2WPKH
   deriving (Show, Eq, Generic, NFData)
 
+-- | Parse a derivation type from @normal@, @compat@ or @segwit@.
 textToDeriveType :: Text -> Maybe DeriveType
 textToDeriveType "normal" = Just DeriveNormal
 textToDeriveType "compat" = Just DeriveP2SH
 textToDeriveType "segwit" = Just DeriveP2WPKH
 textToDeriveType _ = Nothing
 
+-- | Text form of a derivation type: @normal@, @compat@ or @segwit@.
 deriveTypeToText :: DeriveType -> Text
 deriveTypeToText DeriveNormal = "normal"
 deriveTypeToText DeriveP2SH = "compat"
@@ -231,8 +267,11 @@
       Nothing -> Left "invalid derivation type"
       Just x -> Right x
 
+-- | Extended public key with the type of the addresses to derive from it.
 data XPubSpec = XPubSpec
-  { key :: !XPubKey,
+  { -- | the extended public key
+    key :: !XPubKey,
+    -- | type of addresses to derive
     deriv :: !DeriveType
   }
   deriving (Show, Eq, Generic, NFData)
@@ -268,14 +307,17 @@
   put = serialize
   get = deserialize
 
+-- | Seconds since the Unix epoch.
 type UnixTime = Word64
 
+-- | Position of a transaction in a block.
 type BlockPos = Word32
 
 -- | Binary such that ordering is inverted.
 putUnixTime :: (MonadPut m) => Word64 -> m ()
 putUnixTime w = putWord64be $ maxBound - w
 
+-- | Inverse of 'putUnixTime'.
 getUnixTime :: (MonadGet m) => m Word64
 getUnixTime = (maxBound -) <$> getWord64be
 
@@ -287,8 +329,10 @@
         -- | position of transaction within the block
         position :: !Word32
       }
-  | MemRef
-      { timestamp :: !UnixTime
+  | -- | the transaction is in the mempool
+    MemRef
+      { -- | time it entered the mempool
+        timestamp :: !UnixTime
       }
   deriving (Show, Read, Eq, Ord, Generic, Hashable, NFData)
 
@@ -322,6 +366,7 @@
   put = serialize
   get = deserialize
 
+-- | Whether a position is in a block rather than in the mempool.
 confirmed :: BlockRef -> Bool
 confirmed BlockRef {} = True
 confirmed MemRef {} = False
@@ -411,7 +456,8 @@
     utxo :: !Word64,
     -- | number of transactions
     txs :: !Word64,
-    -- | total amount from all outputs in this address
+    -- | total amount from all outputs in this address, including unconfirmed
+    -- and spent ones
     received :: !Word64
   }
   deriving (Show, Read, Eq, Ord, Generic, Hashable, NFData)
@@ -442,6 +488,7 @@
   put = serialize
   get = deserialize
 
+-- | Balance of an address that was never used.
 zeroBalance :: Address -> Balance
 zeroBalance a =
   Balance
@@ -453,6 +500,7 @@
       received = 0
     }
 
+-- | Whether a balance is that of an address that was never used.
 nullBalance :: Balance -> Bool
 nullBalance
   Balance
@@ -498,10 +546,15 @@
 
 -- | Unspent output.
 data Unspent = Unspent
-  { block :: !BlockRef,
+  { -- | position of the transaction that created it
+    block :: !BlockRef,
+    -- | the output
     outpoint :: !OutPoint,
+    -- | amount in satoshi
     value :: !Word64,
+    -- | output script
     script :: !ByteString,
+    -- | address of the output script, if it has one
     address :: !(Maybe Address)
   }
   deriving (Show, Eq, Ord, Generic, Hashable, NFData)
@@ -739,20 +792,34 @@
 
   unmarshalValue net = parseJSON
 
+-- | Transaction input with information about the output it spends.
 data StoreInput
-  = StoreCoinbase
-      { outpoint :: !OutPoint,
+  = -- | input of a coinbase transaction
+    StoreCoinbase
+      { -- | null outpoint
+        outpoint :: !OutPoint,
+        -- | sequence number
         sequence :: !Word32,
+        -- | coinbase data
         script :: !ByteString,
+        -- | witness stack
         witness :: !WitnessStack
       }
-  | StoreInput
-      { outpoint :: !OutPoint,
+  | -- | input that spends an output
+    StoreInput
+      { -- | output spent
+        outpoint :: !OutPoint,
+        -- | sequence number
         sequence :: !Word32,
+        -- | input script
         script :: !ByteString,
+        -- | script of the output spent
         pkscript :: !ByteString,
+        -- | amount of the output spent, in satoshi
         value :: !Word64,
+        -- | witness stack
         witness :: !WitnessStack,
+        -- | address of the output spent, if it has one
         address :: !(Maybe Address)
       }
   deriving (Show, Read, Eq, Ord, Generic, Hashable, NFData)
@@ -801,9 +868,11 @@
   put = serialize
   get = deserialize
 
+-- | Whether a transaction is a coinbase: it has one input, which spends the null outpoint.
 isCoinbaseTx :: Tx -> Bool
 isCoinbaseTx = all ((== nullOutPoint) . (.outpoint)) . (.inputs)
 
+-- | Whether an input is that of a coinbase transaction.
 isCoinbase :: StoreInput -> Bool
 isCoinbase StoreCoinbase {} = True
 isCoinbase StoreInput {} = False
@@ -878,6 +947,7 @@
               >>= maybe (return Nothing) (optional . unmarshalValue net)
           return StoreInput {..}
 
+-- | Parse a hex string in JSON.
 jsonHex :: Text -> Parser ByteString
 jsonHex s =
   case decodeHex s of
@@ -927,9 +997,13 @@
 
 -- | Output information.
 data StoreOutput = StoreOutput
-  { value :: !Word64,
+  { -- | amount in satoshi
+    value :: !Word64,
+    -- | output script
     script :: !ByteString,
+    -- | input that spends it, if spent
     spender :: !(Maybe Spender),
+    -- | address of the output script, if it has one
     address :: !(Maybe Address)
   }
   deriving (Show, Read, Eq, Ord, Generic, Hashable, NFData)
@@ -985,8 +1059,11 @@
           >>= maybe (return Nothing) (optional . unmarshalValue net)
       return StoreOutput {..}
 
+-- | Output spent by an input, as stored with the transaction.
 data Prev = Prev
-  { script :: !ByteString,
+  { -- | output script
+    script :: !ByteString,
+    -- | amount in satoshi
     value :: !Word64
   }
   deriving (Show, Eq, Ord, Generic, Hashable, NFData)
@@ -1008,6 +1085,7 @@
   put = serialize
   get = deserialize
 
+-- | Input with information about the output it spends, which is missing for a coinbase.
 toInput :: Network -> Ctx -> TxIn -> Maybe Prev -> WitnessStack -> StoreInput
 toInput _ _ i Nothing w =
   StoreCoinbase
@@ -1027,6 +1105,7 @@
       address = eitherToMaybe (scriptToAddressBS net ctx p.script)
     }
 
+-- | Output with its address and spender.
 toOutput :: Network -> Ctx -> TxOut -> Maybe Spender -> StoreOutput
 toOutput net ctx o s =
   StoreOutput
@@ -1036,13 +1115,21 @@
       address = eitherToMaybe (scriptToAddressBS net ctx o.script)
     }
 
+-- | Transaction as stored in the database.
 data TxData = TxData
-  { block :: !BlockRef,
+  { -- | position in the chain or mempool
+    block :: !BlockRef,
+    -- | the transaction
     tx :: !Tx,
+    -- | outputs spent by each input, by input index
     prevs :: !(IntMap Prev),
+    -- | the transaction was removed, for example by a double spend
     deleted :: !Bool,
+    -- | the transaction can be replaced by fee
     rbf :: !Bool,
+    -- | time it was first seen, or time of its block
     timestamp :: !Word64,
+    -- | inputs that spend each output, by output index
     spenders :: !(IntMap Spender)
   }
   deriving (Show, Eq, Ord, Generic, NFData)
@@ -1074,6 +1161,7 @@
   put = serialize
   get = deserialize
 
+-- | Fee of a transaction: inputs minus outputs, or zero for a coinbase.
 txDataFee :: TxData -> Word64
 txDataFee t =
   if isCoinbaseTx t.tx
@@ -1124,6 +1212,7 @@
     f n i w = toInput net ctx i (IntMap.lookup n t.prevs) w
     g n o = toOutput net ctx o $ IntMap.lookup n t.spenders
 
+-- | Inverse of 'toTransaction'.
 fromTransaction :: Transaction -> TxData
 fromTransaction t =
   TxData
@@ -1215,6 +1304,7 @@
   put = serialize
   get = deserialize
 
+-- | The transaction without the information added by the store.
 transactionData :: Transaction -> Tx
 transactionData t =
   Tx
@@ -1358,7 +1448,10 @@
 
 -- | Address balances for an extended public key.
 data XPubBal = XPubBal
-  { path :: ![KeyIndex],
+  { -- | derivation path below the xpub: chain (@0@ external, @1@ change) and
+    -- index
+    path :: ![KeyIndex],
+    -- | balance of the address
     balance :: !Balance
   }
   deriving (Show, Ord, Eq, Generic, NFData)
@@ -1402,7 +1495,9 @@
 
 -- | Unspent transaction for extended public key.
 data XPubUnspent = XPubUnspent
-  { unspent :: !Unspent,
+  { -- | the unspent output
+    unspent :: !Unspent,
+    -- | derivation path of its address below the xpub
     path :: ![KeyIndex]
   }
   deriving (Show, Eq, Ord, Generic, NFData)
@@ -1444,12 +1539,19 @@
       path <- o .: "path"
       return XPubUnspent {..}
 
+-- | Summary of the balances of an extended public key.
 data XPubSummary = XPubSummary
-  { confirmed :: !Word64,
+  { -- | confirmed balance in satoshi
+    confirmed :: !Word64,
+    -- | unconfirmed balance in satoshi
     unconfirmed :: !Word64,
+    -- | total received by external addresses, in satoshi
     received :: !Word64,
+    -- | number of unspent outputs
     utxo :: !Word64,
+    -- | index of the last used external address
     external :: !Word32,
+    -- | index of the last used change address
     change :: !Word32
   }
   deriving (Eq, Show, Generic, NFData)
@@ -1527,12 +1629,18 @@
       external <- i .: "external"
       return XPubSummary {..}
 
+-- | Result of a health check.
 class Healthy a where
+  -- | Whether the check passed.
   isOK :: a -> Bool
 
+-- | Whether the database keeps up with the headers.
 data BlockHealth = BlockHealth
-  { headers :: !BlockHeight,
+  { -- | height of the best header
+    headers :: !BlockHeight,
+    -- | height of the best block in the database
     blocks :: !BlockHeight,
+    -- | blocks the database may lag behind
     max :: !Int32
   }
   deriving (Show, Eq, Generic, NFData)
@@ -1587,8 +1695,11 @@
       max <- o .: "max"
       return BlockHealth {..}
 
+-- | Whether something happened recently enough.
 data TimeHealth = TimeHealth
-  { age :: !Int64,
+  { -- | seconds since it happened
+    age :: !Int64,
+    -- | maximum seconds allowed
     max :: !Int64
   }
   deriving (Show, Eq, Generic, NFData)
@@ -1633,8 +1744,11 @@
       max <- o .: "max"
       return TimeHealth {..}
 
+-- | Whether there are enough of something.
 data CountHealth = CountHealth
-  { count :: !Int64,
+  { -- | how many there are
+    count :: !Int64,
+    -- | fewest allowed
     min :: !Int64
   }
   deriving (Show, Eq, Generic, NFData)
@@ -1678,8 +1792,11 @@
       min <- o .: "min"
       return CountHealth {..}
 
+-- | Whether there are few enough of something.
 data MaxHealth = MaxHealth
-  { count :: !Int64,
+  { -- | how many there are
+    count :: !Int64,
+    -- | most allowed
     max :: !Int64
   }
   deriving (Show, Eq, Generic, NFData)
@@ -1723,14 +1840,24 @@
       max <- o .: "max"
       return MaxHealth {..}
 
+-- | Health of the store, as returned by @\/health@. It is healthy if every check
+-- passes.
 data HealthCheck = HealthCheck
-  { blocks :: !BlockHealth,
+  { -- | the database keeps up with the headers
+    blocks :: !BlockHealth,
+    -- | the best header is recent
     lastBlock :: !TimeHealth,
+    -- | a transaction or block arrived recently
     lastTx :: !TimeHealth,
+    -- | few transactions wait to be imported
     pendingTxs :: !MaxHealth,
+    -- | enough peers are connected
     peers :: !CountHealth,
+    -- | network name
     network :: !String,
+    -- | version of the software
     version :: !String,
+    -- | time of the check
     time :: !Word64
   }
   deriving (Show, Eq, Generic, NFData)
@@ -1806,9 +1933,12 @@
       let time = round $ utcTimeToPOSIXSeconds utcTime
       return HealthCheck {..}
 
+-- | Event streamed by @\/events@.
 data Event
-  = EventBlock !BlockHash
-  | EventTx !TxHash
+  = -- | a new best block
+    EventBlock !BlockHash
+  | -- | a transaction entered or left the mempool
+    EventTx !TxHash
   deriving (Show, Eq, Generic, NFData)
 
 instance Serial Event where
@@ -1851,6 +1981,7 @@
           return $ EventBlock i
         _ -> fail $ "Could not recognize event type: " <> t
 
+-- | Response that wraps a single value, encoded in JSON as @{\"result\": value}@.
 newtype GenericResult a = GenericResult {get :: a}
   deriving (Show, Eq, Generic, NFData)
 
@@ -1874,6 +2005,8 @@
   parseJSON =
     A.withObject "GenericResult" $ \o -> GenericResult <$> o .: "result"
 
+-- | Response with a value in its peer-to-peer serialization, encoded in JSON as
+-- a hex string in @{\"result\": hex}@.
 newtype RawResult a = RawResult {get :: a}
   deriving (Show, Eq, Generic, NFData)
 
@@ -1904,6 +2037,7 @@
               =<< decodeHex res
       maybe mzero (return . RawResult) m
 
+-- | List response. In binary, a length followed by the items.
 newtype SerialList a = SerialList {get :: [a]}
   deriving (Show, Eq, Generic, NFData)
 
@@ -1924,6 +2058,8 @@
 instance (FromJSON a) => FromJSON (SerialList a) where
   parseJSON = fmap SerialList . parseJSON
 
+-- | List of values in their peer-to-peer serialization, encoded in JSON as a
+-- list of hex strings.
 newtype RawResultList a = RawResultList {get :: [a]}
   deriving (Show, Eq, Generic, NFData)
 
@@ -1967,6 +2103,8 @@
         A.withText "RawResultListItem" $ \t ->
           maybe mzero return (decodeHex t >>= deser)
 
+-- | Hash of a transaction, as returned when posting one, encoded in JSON as
+-- @{\"txid\": hash}@.
 newtype TxId
   = TxId TxHash
   deriving (Show, Eq, Generic, NFData)
@@ -1990,15 +2128,24 @@
 instance FromJSON TxId where
   parseJSON = A.withObject "txid" $ \o -> TxId <$> o .: "txid"
 
+-- | Errors returned by the API.
 data Except
-  = ThingNotFound
-  | ServerError
-  | BadRequest
-  | UserError !String
-  | StringError !String
-  | TxIndexConflict ![TxHash]
-  | ServerTimeout
-  | RequestTooLarge
+  = -- | the resource does not exist (404)
+    ThingNotFound
+  | -- | internal error (500)
+    ServerError
+  | -- | the request is malformed (400)
+    BadRequest
+  | -- | the request is invalid, with a reason (400)
+    UserError !String
+  | -- | other error, with a message (400)
+    StringError !String
+  | -- | a short transaction number matches these transactions (409)
+    TxIndexConflict ![TxHash]
+  | -- | the request took too long (500)
+    ServerTimeout
+  | -- | the request asks for too much (413)
+    RequestTooLarge
   deriving (Show, Eq, Ord, Generic, NFData)
 
 instance Exception Except
@@ -2070,15 +2217,21 @@
 -- Blockchain.info API Compatibility --
 ---------------------------------------
 
+-- | Short numeric identifier of a transaction, as used by Blockchain.info: the
+-- first eight bytes of its hash, read as a big-endian number, shifted right by
+-- eleven bits.
 toIntTxId :: TxHash -> Word64
 toIntTxId h =
   let bs = runPutS (serialize h)
       Right w64 = runGetS getWord64be bs
    in w64 `shift` (-11)
 
+-- | Block identified by hash or by height.
 data BinfoBlockId
-  = BinfoBlockHash !BlockHash
-  | BinfoBlockIndex !Word32
+  = -- | by hash
+    BinfoBlockHash !BlockHash
+  | -- | by height
+    BinfoBlockIndex !Word32
   deriving (Eq, Show, Read, Generic, NFData)
 
 instance Parsable BinfoBlockId where
@@ -2090,11 +2243,15 @@
         Just h -> Right $ BinfoBlockHash h
       igr = BinfoBlockIndex <$> parseParam t
 
+-- | Transaction identified by hash or by short number; see 'toIntTxId'.
 data BinfoTxId
-  = BinfoTxIdHash !TxHash
-  | BinfoTxIdIndex !Word64
+  = -- | by hash
+    BinfoTxIdHash !TxHash
+  | -- | by short number
+    BinfoTxIdIndex !Word64
   deriving (Eq, Show, Read, Generic, NFData)
 
+-- | Identify a transaction by short number if the flag is 'True', or by hash otherwise.
 encodeBinfoTxId :: Bool -> TxHash -> BinfoTxId
 encodeBinfoTxId False = BinfoTxIdHash
 encodeBinfoTxId True = BinfoTxIdIndex . toIntTxId
@@ -2120,13 +2277,20 @@
     BinfoTxIdHash <$> parseJSON v
       <|> BinfoTxIdIndex <$> parseJSON v
 
+-- | Which transactions to list in a Blockchain.info response (@filter@).
 data BinfoFilter
-  = BinfoFilterAll
-  | BinfoFilterSent
-  | BinfoFilterReceived
-  | BinfoFilterMoved
-  | BinfoFilterConfirmed
-  | BinfoFilterMempool
+  = -- | all of them (@0@ or @6@)
+    BinfoFilterAll
+  | -- | those that decrease the balance (@1@)
+    BinfoFilterSent
+  | -- | those that increase the balance (@2@)
+    BinfoFilterReceived
+  | -- | those that leave the balance unchanged except for fees (@3@)
+    BinfoFilterMoved
+  | -- | confirmed ones (@5@)
+    BinfoFilterConfirmed
+  | -- | unconfirmed ones (@7@)
+    BinfoFilterMempool
   deriving (Eq, Show, Generic, NFData)
 
 instance Parsable BinfoFilter where
@@ -2141,12 +2305,19 @@
       7 -> return BinfoFilterMempool
       _ -> Left "could not parse filter parameter"
 
+-- | Response of @\/blockchain\/multiaddr@.
 data BinfoMultiAddr = BinfoMultiAddr
-  { addresses :: ![BinfoBalance],
+  { -- | balance of each address and xpub requested
+    addresses :: ![BinfoBalance],
+    -- | totals of all of them
     wallet :: !BinfoWallet,
+    -- | page of their transactions, newest first
     txs :: ![BinfoTx],
+    -- | information about the chain and prices
     info :: !BinfoInfo,
+    -- | always 'True'
     recommendFee :: !Bool,
+    -- | addresses are shown in CashAddr format
     cashAddr :: !Bool
   }
   deriving (Eq, Show, Generic, NFData)
@@ -2187,13 +2358,21 @@
     where
       net = if not m.cashAddr && net' == bch then btc else net'
 
+-- | Response of @\/blockchain\/rawaddr\/:addr@.
 data BinfoRawAddr = BinfoRawAddr
-  { address :: !BinfoAddr,
+  { -- | address or xpub
+    address :: !BinfoAddr,
+    -- | balance in satoshi
     balance :: !Word64,
+    -- | number of transactions
     ntx :: !Word64,
+    -- | number of unspent outputs
     utxo :: !Word64,
+    -- | total received in satoshi
     received :: !Word64,
+    -- | total sent in satoshi
     sent :: !Int64,
+    -- | page of transactions, newest first
     txs :: ![BinfoTx]
   }
   deriving (Eq, Show, Generic, NFData)
@@ -2256,9 +2435,13 @@
       txs <- mapM (unmarshalValue (net, ctx)) =<< o .: "txs"
       return BinfoRawAddr {..}
 
+-- | Balance of an address or xpub in @\/blockchain\/balance@.
 data BinfoShortBal = BinfoShortBal
-  { final :: !Word64,
+  { -- | balance in satoshi
+    final :: !Word64,
+    -- | number of transactions
     ntx :: !Word64,
+    -- | total received in satoshi
     received :: !Word64
   }
   deriving (Eq, Show, Read, Generic, NFData)
@@ -2286,21 +2469,36 @@
       received <- o .: "total_received"
       return BinfoShortBal {..}
 
+-- | Balance of an address or xpub in @\/blockchain\/multiaddr@.
 data BinfoBalance
-  = BinfoAddrBalance
-      { address :: !Address,
+  = -- | balance of an address
+    BinfoAddrBalance
+      { -- | the address
+        address :: !Address,
+        -- | number of transactions
         txs :: !Word64,
+        -- | total received in satoshi
         received :: !Word64,
+        -- | total sent in satoshi
         sent :: !Word64,
+        -- | balance in satoshi
         balance :: !Word64
       }
-  | BinfoXPubBalance
-      { xpub :: !XPubKey,
+  | -- | balance of an xpub
+    BinfoXPubBalance
+      { -- | the xpub
+        xpub :: !XPubKey,
+        -- | number of transactions
         txs :: !Word64,
+        -- | total received in satoshi
         received :: !Word64,
+        -- | total sent in satoshi
         sent :: !Word64,
+        -- | balance in satoshi
         balance :: !Word64,
+        -- | index of the last used external address
         external :: !Word32,
+        -- | index of the last used change address
         change :: !Word32
       }
   deriving (Eq, Show, Generic, NFData)
@@ -2375,11 +2573,17 @@
     A.withArray "[BinfoBalance]" $
       fmap V.toList . mapM (unmarshalValue (net, ctx))
 
+-- | Totals of all addresses and xpubs in @\/blockchain\/multiaddr@.
 data BinfoWallet = BinfoWallet
-  { balance :: !Word64,
+  { -- | balance in satoshi
+    balance :: !Word64,
+    -- | number of transactions
     txs :: !Word64,
+    -- | number of transactions that pass the filter
     filtered :: !Word64,
+    -- | total received in satoshi
     received :: !Word64,
+    -- | total sent in satoshi
     sent :: !Word64
   }
   deriving (Eq, Show, Generic, NFData)
@@ -2413,6 +2617,7 @@
       sent <- o .: "total_sent"
       return BinfoWallet {..}
 
+-- | Amount as a big-endian hex string without leading zeros, as Blockchain.info shows values of unspent outputs.
 binfoHexValue :: Word64 -> Text
 binfoHexValue w64 =
   encodeHex $
@@ -2425,13 +2630,21 @@
         runPutS $
           serialize w64
 
+-- | Unspent output in @\/blockchain\/unspent@.
 data BinfoUnspent = BinfoUnspent
-  { txid :: !TxHash,
+  { -- | hash of the transaction
+    txid :: !TxHash,
+    -- | index of the output
     index :: !Word32,
+    -- | output script
     script :: !ByteString,
+    -- | amount in satoshi
     value :: !Word64,
+    -- | number of confirmations; @0@ if unconfirmed
     confirmations :: !Int32,
+    -- | identifier of the transaction
     txidx :: !BinfoTxId,
+    -- | xpub and path of its address, if it belongs to an xpub requested
     xpub :: !(Maybe BinfoXPubPath)
   }
   deriving (Eq, Show, Generic, NFData)
@@ -2477,6 +2690,7 @@
       xpub <- mapM (unmarshalValue (net, ctx)) =<< o .:? "xpub"
       return BinfoUnspent {..}
 
+-- | Response of @\/blockchain\/unspent@.
 newtype BinfoUnspents = BinfoUnspents [BinfoUnspent]
   deriving (Eq, Show, Generic, NFData)
 
@@ -2499,6 +2713,7 @@
       us <- mapM (unmarshalValue (net, ctx)) =<< o .: "unspent_outputs"
       return (BinfoUnspents us)
 
+-- | Block in the Blockchain.info format, from its data, its transactions and the hashes of its children.
 toBinfoBlock :: BlockData -> [BinfoTx] -> [BlockHash] -> BinfoBlock
 toBinfoBlock b transactions next_blocks =
   BinfoBlock
@@ -2520,22 +2735,39 @@
       txs = transactions
     }
 
+-- | Block in the Blockchain.info format.
 data BinfoBlock = BinfoBlock
-  { hash :: !BlockHash,
+  { -- | block hash
+    hash :: !BlockHash,
+    -- | block version
     version :: !Word32,
+    -- | hash of the previous block
     prev :: !BlockHash,
+    -- | Merkle root of the transactions
     merkle :: !Hash256,
+    -- | block timestamp
     timestamp :: !Word32,
+    -- | difficulty target in compact form
     bits :: !Word32,
+    -- | hashes of the children of the block
     next :: ![BlockHash],
+    -- | total fees in satoshi
     fee :: !Word64,
+    -- | nonce
     nonce :: !Word32,
+    -- | number of transactions
     ntx :: !Word32,
+    -- | size in bytes
     size :: !Word32,
+    -- | block index; the height
     index :: !Word32,
+    -- | whether it is in the main chain
     main :: !Bool,
+    -- | height
     height :: !Word32,
+    -- | weight
     weight :: !Word32,
+    -- | transactions
     txs :: ![BinfoTx]
   }
   deriving (Eq, Show, Generic, NFData)
@@ -2613,24 +2845,44 @@
     A.withObject "blocks" $ \o ->
       mapM (unmarshalValue (net, ctx)) =<< o .: "blocks"
 
+-- | Transaction in the Blockchain.info format.
 data BinfoTx = BinfoTx
-  { txid :: !TxHash,
+  { -- | hash
+    txid :: !TxHash,
+    -- | version
     version :: !Word32,
+    -- | number of inputs
     inputCount :: !Word32,
+    -- | number of outputs
     outputCount :: !Word32,
+    -- | size in bytes
     size :: !Word32,
+    -- | weight
     weight :: !Word32,
+    -- | fee in satoshi
     fee :: !Word64,
+    -- | IP address of the peer that relayed it; always @0.0.0.0@
     relayed :: !ByteString,
+    -- | lock time
     locktime :: !Word32,
+    -- | identifier
     index :: !BinfoTxId,
+    -- | whether it was removed by a double spend
     doubleSpend :: !Bool,
+    -- | whether it can be replaced by fee
     rbf :: !Bool,
+    -- | net change to the balance of the addresses requested, and the
+    -- balance after it; only in responses about addresses
     balance :: !(Maybe (Int64, Int64)),
+    -- | time first seen, or time of its block
     timestamp :: !Word64,
+    -- | height of its block, if confirmed
     blockIndex :: !(Maybe Word32),
+    -- | height of its block, if confirmed
     blockHeight :: !(Maybe Word32),
+    -- | inputs
     inputs :: ![BinfoTxInput],
+    -- | outputs
     outputs :: ![BinfoTxOutput]
   }
   deriving (Eq, Show, Generic, NFData)
@@ -2726,11 +2978,17 @@
     A.withArray "[BinfoTx]" $
       fmap V.toList . mapM (unmarshalValue (net, ctx))
 
+-- | Transaction input in the Blockchain.info format.
 data BinfoTxInput = BinfoTxInput
-  { sequence :: !Word32,
+  { -- | sequence number
+    sequence :: !Word32,
+    -- | serialized witness stack
     witness :: !ByteString,
+    -- | input script
     script :: !ByteString,
+    -- | index of the input
     index :: !Word32,
+    -- | output that it spends
     output :: !BinfoTxOutput
   }
   deriving (Eq, Show, Generic, NFData)
@@ -2770,15 +3028,25 @@
           >>= unmarshalValue (net, ctx)
       return BinfoTxInput {..}
 
+-- | Transaction output in the Blockchain.info format.
 data BinfoTxOutput = BinfoTxOutput
-  { typ :: !Int,
+  { -- | type; always @0@
+    typ :: !Int,
+    -- | whether it is spent
     spent :: !Bool,
+    -- | amount in satoshi
     value :: !Word64,
+    -- | index of the output
     index :: !Word32,
+    -- | identifier of its transaction
     txidx :: !BinfoTxId,
+    -- | output script
     script :: !ByteString,
+    -- | inputs that spend it
     spenders :: ![BinfoSpender],
+    -- | address of the output script, if it has one
     address :: !(Maybe Address),
+    -- | xpub and path of its address, if it belongs to an xpub requested
     xpub :: !(Maybe BinfoXPubPath)
   }
   deriving (Eq, Show, Generic, NFData)
@@ -2832,8 +3100,11 @@
       xpub <- o .:? "xpub" >>= mapM (unmarshalValue (net, ctx))
       return BinfoTxOutput {..}
 
+-- | Input that spends an output, in the Blockchain.info format.
 data BinfoSpender = BinfoSpender
-  { txidx :: !BinfoTxId,
+  { -- | identifier of the spending transaction
+    txidx :: !BinfoTxId,
+    -- | index of the input
     input :: !Word32
   }
   deriving (Eq, Show, Generic, NFData)
@@ -2858,8 +3129,11 @@
       input <- o .: "n"
       return BinfoSpender {..}
 
+-- | Xpub that an address belongs to, with its derivation path.
 data BinfoXPubPath = BinfoXPubPath
-  { key :: !XPubKey,
+  { -- | the xpub
+    key :: !XPubKey,
+    -- | derivation path of the address below the xpub
     deriv :: !SoftPath
   }
   deriving (Eq, Show, Generic, NFData)
@@ -2892,11 +3166,17 @@
       deriv <- fromMaybe "bad xpub path" . parseSoft <$> o .: "path"
       return BinfoXPubPath {..}
 
+-- | Information about the chain and prices in @\/blockchain\/multiaddr@.
 data BinfoInfo = BinfoInfo
-  { connected :: !Word32,
+  { -- | number of connected peers
+    connected :: !Word32,
+    -- | satoshi per unit of the fiat currency
     conversion :: !Double,
+    -- | fiat currency
     fiat :: !BinfoSymbol,
+    -- | coin of the network
     crypto :: !BinfoSymbol,
+    -- | best block
     head :: !BinfoBlockInfo
   }
   deriving (Eq, Show, Generic, NFData)
@@ -2930,10 +3210,15 @@
       head <- o .: "latest_block"
       return BinfoInfo {..}
 
+-- | Short information about a block in the Blockchain.info format.
 data BinfoBlockInfo = BinfoBlockInfo
-  { hash :: !BlockHash,
+  { -- | block hash
+    hash :: !BlockHash,
+    -- | height
     height :: !BlockHeight,
+    -- | block timestamp
     timestamp :: !Word32,
+    -- | block index; the height
     index :: !BlockHeight
   }
   deriving (Eq, Show, Generic, NFData)
@@ -2964,6 +3249,7 @@
       index <- o .: "block_index"
       return BinfoBlockInfo {..}
 
+-- | Short information about a block.
 toBinfoBlockInfo :: BlockData -> BinfoBlockInfo
 toBinfoBlockInfo d =
   BinfoBlockInfo
@@ -2973,9 +3259,13 @@
       index = d.height
     }
 
+-- | Historical price of the coin of the network, from the price service.
 data BinfoRate = BinfoRate
-  { timestamp :: !Word64,
+  { -- | time of the price
+    timestamp :: !Word64,
+    -- | price in the fiat currency
     price :: !Double,
+    -- | volume in the 24 hours before
     vol24 :: !Double
   }
   deriving (Eq, Show, Generic, NFData)
@@ -3003,15 +3293,25 @@
       vol24 <- o .: "volume24h"
       return BinfoRate {..}
 
+-- | Entry of @\/blockchain\/export-history@.
 data BinfoHistory = BinfoHistory
-  { date :: !Text,
+  { -- | date, as @YYYY-MM-DD@
+    date :: !Text,
+    -- | time of day
     time :: !Text,
+    -- | @sent@ or @received@
     typ :: !Text,
+    -- | amount in coins
     amount :: !Double,
+    -- | value in fiat at the time
     valueThen :: !Double,
+    -- | value in fiat now
     valueNow :: !Double,
+    -- | price at the time
     rateThen :: !Double,
+    -- | transaction hash
     txid :: !TxHash,
+    -- | fee in coins
     fee :: !Double
   }
   deriving (Eq, Show, Generic, NFData)
@@ -3057,6 +3357,8 @@
       fee <- o .: "fee"
       return BinfoHistory {..}
 
+-- | History entry from a net amount in satoshi, a time, the price then and now, a
+-- fee in satoshi, and a transaction hash.
 toBinfoHistory ::
   Int64 ->
   Word64 ->
@@ -3086,6 +3388,8 @@
     v1 = v * r1
     v2 = v * r2
 
+-- | Date given as @DD-MM-YYYY@ or @DD\/MM\/YYYY@ in a request, as seconds
+-- since the Unix epoch.
 newtype BinfoDate = BinfoDate Word64
   deriving (Eq, Show, Read, Generic, NFData)
 
@@ -3098,11 +3402,18 @@
       s = LazyText.unpack t
       p fmt = parseTimeM False defaultTimeLocale fmt s
 
+-- | Price of the coin of the network in a fiat currency, from the ticker
+-- service.
 data BinfoTicker = BinfoTicker
-  { fifteen :: !Double,
+  { -- | price delayed by fifteen minutes
+    fifteen :: !Double,
+    -- | last price
     last :: !Double,
+    -- | buy price
     buy :: !Double,
+    -- | sell price
     sell :: !Double,
+    -- | currency symbol
     symbol :: !Text
   }
   deriving (Eq, Show, Generic, NFData)
@@ -3146,12 +3457,19 @@
       last <- o .: "last"
       return BinfoTicker {..}
 
+-- | Currency in the Blockchain.info format.
 data BinfoSymbol = BinfoSymbol
-  { code :: !Text,
+  { -- | currency code, such as @USD@
+    code :: !Text,
+    -- | symbol, such as @$@
     symbol :: !Text,
+    -- | name
     name :: !Text,
+    -- | satoshi per unit
     conversion :: !Double,
+    -- | whether the symbol goes after the amount
     after :: !Bool,
+    -- | whether it is a fiat currency
     local :: !Bool
   }
   deriving (Eq, Show, Generic, NFData)
@@ -3199,6 +3517,8 @@
       local <- o .: "local"
       return BinfoSymbol {..}
 
+-- | Transactions to fetch to show a transaction: those it spends, and, if outputs
+-- are pruned, those that spend its outputs to other addresses.
 relevantTxs ::
   HashSet Address ->
   Bool ->
@@ -3221,6 +3541,7 @@
     g StoreInput {outpoint = OutPoint h i} = Just h
     ins = mapMaybe g t.inputs
 
+-- | Balances of addresses and xpubs in the Blockchain.info format.
 toBinfoAddrs ::
   HashMap Address Balance ->
   HashMap XPubSpec [XPubBal] ->
@@ -3262,6 +3583,7 @@
              in BinfoAddrBalance {..}
        in map f $ HashMap.elems onlyAddrs
 
+-- | Transaction in the Blockchain.info format, without balance information.
 toBinfoTxSimple ::
   Bool ->
   Transaction ->
@@ -3269,6 +3591,7 @@
 toBinfoTxSimple numtxid =
   toBinfoTx numtxid HashMap.empty False 0
 
+-- | Inputs of a transaction in the Blockchain.info format.
 toBinfoTxInputs ::
   Bool ->
   HashMap Address (Maybe BinfoXPubPath) ->
@@ -3297,11 +3620,16 @@
       putVarInt (B.length bs)
       putByteString bs
 
+-- | Height of the block of a transaction, or 'Nothing' if unconfirmed.
 transactionHeight :: Transaction -> Maybe BlockHeight
 transactionHeight Transaction {deleted = True} = Nothing
 transactionHeight Transaction {block = MemRef _} = Nothing
 transactionHeight Transaction {block = BlockRef h _} = Just h
 
+-- | Transaction in the Blockchain.info format. Arguments: identify transactions
+-- by short number; address book of the addresses requested and their xpubs;
+-- prune outputs to other addresses of a transaction that increases the balance;
+-- balance after the transaction.
 toBinfoTx ::
   Bool ->
   HashMap Address (Maybe BinfoXPubPath) ->
@@ -3341,6 +3669,7 @@
     aset = HashMap.keysSet abook
     simple = HashMap.null abook && bal == 0
 
+-- | Net change that a transaction makes to the balance of a set of addresses.
 getTxResult :: HashSet Address -> Transaction -> Int64
 getTxResult aset t =
   inputSum + outputSum
@@ -3364,6 +3693,7 @@
             then fromIntegral value
             else 0
 
+-- | Output in the Blockchain.info format, or 'Nothing' if it is pruned.
 toBinfoTxOutput ::
   Bool ->
   HashMap Address (Maybe BinfoXPubPath) ->
@@ -3391,6 +3721,7 @@
   where
     notInBook = isNothing $ o.address >>= flip HashMap.lookup abook
 
+-- | Spender in the Blockchain.info format.
 toBinfoSpender :: Bool -> Spender -> BinfoSpender
 toBinfoSpender numtxid s =
   BinfoSpender
@@ -3398,6 +3729,7 @@
       input = s.index
     }
 
+-- | Output spent by an input, in the Blockchain.info format.
 inputToBinfoTxOutput ::
   Bool ->
   HashMap Address (Maybe BinfoXPubPath) ->
@@ -3434,11 +3766,15 @@
           StoreInput {address} -> address >>= join . flip HashMap.lookup abook
     }
 
+-- | Address or xpub in a Blockchain.info request.
 data BinfoAddr
-  = BinfoAddr !Address
-  | BinfoXpub !XPubKey
+  = -- | an address
+    BinfoAddr !Address
+  | -- | an xpub
+    BinfoXpub !XPubKey
   deriving (Eq, Show, Generic, Hashable, NFData)
 
+-- | Parse addresses and xpubs separated by pipes or commas. Returns 'Nothing' if any is invalid.
 parseBinfoAddr :: Network -> Ctx -> Text -> Maybe [BinfoAddr]
 parseBinfoAddr net ctx "" = Just []
 parseBinfoAddr net ctx s =
@@ -3449,11 +3785,17 @@
       BinfoAddr <$> textToAddr net x
         <|> BinfoXpub <$> xPubImport net ctx x
 
+-- | Block header with its transactions in the Blockchain.info format.
 data BinfoHeader = BinfoHeader
-  { hash :: !BlockHash,
+  { -- | block hash
+    hash :: !BlockHash,
+    -- | block timestamp
     timestamp :: !Timestamp,
+    -- | block index; the height
     index :: !Word32,
+    -- | height
     height :: !BlockHeight,
+    -- | identifiers of its transactions
     txids :: ![BinfoTxId]
   }
   deriving (Eq, Show, Generic, NFData)
@@ -3487,6 +3829,7 @@
       txids <- o .: "txIndexes"
       return BinfoHeader {..}
 
+-- | Response of @\/blockchain\/unconfirmed-transactions@.
 newtype BinfoMempool = BinfoMempool {get :: [BinfoTx]}
   deriving (Eq, Show, Generic, NFData)
 
@@ -3501,6 +3844,7 @@
     A.withObject "BinfoMempool" $ \o ->
       BinfoMempool <$> (mapM (unmarshalValue (net, ctx)) =<< o .: "txs")
 
+-- | List of short block information.
 newtype BinfoBlockInfos = BinfoBlockInfos {get :: [BinfoBlockInfo]}
   deriving (Eq, Show, Generic, NFData)
 
diff --git a/src/Haskoin/Store/WebClient.hs b/src/Haskoin/Store/WebClient.hs
--- a/src/Haskoin/Store/WebClient.hs
+++ b/src/Haskoin/Store/WebClient.hs
@@ -8,11 +8,39 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE NoFieldSelectors #-}
 
+-- |
+-- Module      : Haskoin.Store.WebClient
+-- Copyright   : (c) Haskoin Developers
+-- License     : MIT
+-- Maintainer  : jprupp@protonmail.ch
+-- Stability   : experimental
+-- Portability : POSIX
+--
+-- Client for the REST API of Haskoin Store.
+--
+-- Build a request value, such as 'GetAddrBalance', and pass it to 'apiCall'.
+-- The response type follows from the request type. Requests and responses use
+-- the compact binary encoding of the API. Errors returned by the server are
+-- thrown as 'Store.Except' in 'MonadError'.
+--
+-- @
+-- import Control.Monad.Except (runExceptT)
+-- import Data.Default (def)
+--
+-- balance :: Ctx -> Address -> IO (Either Except Balance)
+-- balance ctx addr = runExceptT $ 'apiCall' ctx def ('GetAddrBalance' addr)
+-- @
+--
+-- Requests that take long lists of addresses, transactions or blocks can be
+-- split into several calls with 'apiBatch'.
+-- The client always uses the binary encoding, which is smaller and faster than
+-- JSON and decodes straight into the types of "Haskoin.Store.Data".
 module Haskoin.Store.WebClient
-  ( ApiConfig (..),
+  ( -- * Calls
+    ApiConfig (..),
     apiCall,
     apiBatch,
-    -- Blocks
+    -- * Blocks
     GetBlock (..),
     GetBlocks (..),
     GetBlockRaw (..),
@@ -24,7 +52,7 @@
     GetBlockHeightRaw (..),
     GetBlockTime (..),
     GetBlockTimeRaw (..),
-    -- Transactions
+    -- * Transactions
     GetTx (..),
     GetTxs (..),
     GetTxRaw (..),
@@ -35,7 +63,7 @@
     PostTx (..),
     GetMempool (..),
     GetEvents (..),
-    -- Address
+    -- * Addresses
     GetAddrTxs (..),
     GetAddrsTxs (..),
     GetAddrTxsFull (..),
@@ -44,17 +72,17 @@
     GetAddrsBalance (..),
     GetAddrUnspent (..),
     GetAddrsUnspent (..),
-    -- XPubs
+    -- * Extended public keys
     GetXPub (..),
     GetXPubTxs (..),
     GetXPubTxsFull (..),
     GetXPubBalances (..),
     GetXPubUnspent (..),
     DelCachedXPub (..),
-    -- Network
+    -- * Network
     GetPeers (..),
     GetHealth (..),
-    -- Params
+    -- * Parameters
     StartParam (..),
     OffsetParam (..),
     LimitParam (..),
@@ -105,7 +133,9 @@
 -- }
 -- @
 data ApiConfig = ApiConfig
-  { net :: !Network,
+  { -- | network to query; its name is appended to the host
+    net :: !Network,
+    -- | base URL of the API, ending in a slash
     host :: !String
   }
   deriving (Eq, Show)
@@ -125,7 +155,9 @@
 --
 -- With options:
 --
--- > apiCall ctx def $ GetAddrsUnspent addrs def{ paramLimit = Just 10 }
+-- > apiCall ctx def $ GetAddrsUnspent addrs def {limit = Just 10}
+--
+-- Only @GET@ and @POST@ endpoints are supported.
 apiCall ::
   (ApiResource a b, MonadIO m, MonadError Store.Except m) =>
   Ctx ->
@@ -146,7 +178,10 @@
 
 -- | Batch commands that have a large list of arguments:
 --
--- > apiBatch 20 def (GetAddrsTxs addrs def)
+-- > apiBatch ctx 20 def (GetAddrsTxs addrs def)
+--
+-- The list is split into chunks of the given size, one call is made per chunk,
+-- and the results are concatenated.
 apiBatch ::
   (Batchable a b, MonadIO m, MonadError Store.Except m) =>
   Ctx ->
@@ -157,7 +192,10 @@
 apiBatch ctx i conf res =
   mconcat <$> mapM (apiCall ctx conf) (resourceBatch i res)
 
+-- | Requests that can be split into several smaller ones whose results are
+-- concatenated.
 class (ApiResource a b, Monoid b) => Batchable a b where
+  -- | Split a request into requests with lists of at most this size.
   resourceBatch :: Natural -> a -> [a]
 
 instance Batchable GetBlocks (Store.SerialList Store.BlockData) where
@@ -197,6 +235,7 @@
 -- API Internal --
 ------------------
 
+-- | HTTP options carrying the query string parameters of a request.
 toOptions ::
   (ApiResource a b) =>
   Network ->
@@ -208,6 +247,7 @@
   where
     f (ParamBox p) = toOption net ctx p
 
+-- | HTTP option for one query string parameter.
 toOption ::
   (Param a) =>
   Network ->
@@ -220,9 +260,11 @@
       encodeParam net ctx a
   return $ applyOpt (paramLabel a) res
 
+-- | Set a query string parameter to comma-separated values.
 applyOpt :: Text -> [Text] -> Endo HTTP.Options
 applyOpt p t = Endo $ HTTP.param p .~ [Text.intercalate "," t]
 
+-- | Make a @GET@ request and decode the binary response.
 getBinary ::
   (Serial a) =>
   Endo HTTP.Options ->
@@ -234,6 +276,7 @@
     res <- resE
     return . runGetL deserialize $ res ^. HTTP.responseBody
 
+-- | Make a @POST@ request with a binary body and decode the binary response.
 postBinary ::
   (Serial a, Serial r) =>
   Endo HTTP.Options ->
@@ -246,6 +289,7 @@
     res <- resE
     return . runGetL deserialize $ res ^. HTTP.responseBody
 
+-- | Request binary responses, and turn error responses into exceptions.
 binaryOpts :: Endo HTTP.Options -> HTTP.Options
 binaryOpts opts =
   appEndo (opts <> accept <> stat) HTTP.defaults
@@ -253,6 +297,9 @@
     accept = Endo $ HTTP.header "Accept" .~ ["application/octet-stream"]
     stat = Endo $ HTTP.checkResponse ?~ checkStatus
 
+-- | Throw the error in the body of an unsuccessful response as 'Store.Except'.
+-- A health check that answers 503 is not an error: its body says what is
+-- wrong.
 checkStatus :: ResponseChecker
 checkStatus req res
   | statusIsSuccessful status = return ()
@@ -273,6 +320,7 @@
 -- Utilities --
 ---------------
 
+-- | Split a list into chunks of at most this size.
 chunksOf :: Natural -> [a] -> [[a]]
 chunksOf n xs
   | null xs = []
diff --git a/src/Haskoin/Store/WebCommon.hs b/src/Haskoin/Store/WebCommon.hs
--- a/src/Haskoin/Store/WebCommon.hs
+++ b/src/Haskoin/Store/WebCommon.hs
@@ -10,6 +10,27 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE NoFieldSelectors #-}
 
+-- |
+-- Module      : Haskoin.Store.WebCommon
+-- Copyright   : (c) Haskoin Developers
+-- License     : MIT
+-- Maintainer  : jprupp@protonmail.ch
+-- Stability   : experimental
+-- Portability : POSIX
+--
+-- Description of the REST API, shared by the server ("Haskoin.Store.Web") and
+-- the client ("Haskoin.Store.WebClient").
+--
+-- Each endpoint is a type, such as 'GetBlock', with an 'ApiResource' instance
+-- that gives its HTTP method, path, parameters and response type. Parameters
+-- are types with a 'Param' instance, which knows how to encode them in a URL
+-- and parse them back.
+--
+-- Endpoints that return lists take a limit, an offset and a start
+-- ('LimitsParam'). Results are ordered from newest to oldest.
+-- Describing each endpoint once, as a type, keeps the server and the client from
+-- drifting apart: the path, parameters and response type that the server routes
+-- are the same ones the client builds requests from.
 module Haskoin.Store.WebCommon where
 
 import Control.Applicative ((<|>))
@@ -42,21 +63,34 @@
 -- API Resources --
 -------------------
 
+-- | An endpoint of the API. The request type @a@ determines the response type
+-- @b@.
 class (Serial b) => ApiResource a b | a -> b where
+  -- | HTTP method; 'GET' by default.
   resourceMethod :: Proxy a -> StdMethod
   resourceMethod _ = GET
+  -- | Path of the endpoint, given the encoded values of the parameters that
+  -- are part of the path.
   resourcePath :: Proxy a -> ([Text] -> Text)
+  -- | Parameters of a request: those that go in the path, and those that go in
+  -- the query string.
   queryParams :: a -> ([ParamBox], [ParamBox]) -- (resource, querystring)
   queryParams _ = ([], [])
+  -- | Types of the parameters that go in the path, used to build the route
+  -- pattern of the server.
   captureParams :: Proxy a -> [ProxyBox]
   captureParams _ = []
+  -- | Body of the request, if any.
   resourceBody :: a -> Maybe PostBox
   resourceBody = const Nothing
 
+-- | Request body of any serializable type.
 data PostBox = forall s. (Serial s) => PostBox !s
 
+-- | Parameter value of any type.
 data ParamBox = forall p. (Eq p, Param p) => ParamBox !p
 
+-- | Parameter type, without a value.
 data ProxyBox = forall p. (Param p) => ProxyBox !(Proxy p)
 
 --------------------
@@ -64,86 +98,125 @@
 --------------------
 
 -- Blocks
+-- | Request for a block by hash: @GET \/block\/:block@.
 data GetBlock = GetBlock !BlockHash !NoTx
 
+-- | Request for several blocks by hash: @GET \/blocks?blocks=…@.
 data GetBlocks = GetBlocks ![BlockHash] !NoTx
 
+-- | Request for a block by hash, serialized as in the peer-to-peer protocol: @GET \/block\/:block\/raw@.
 newtype GetBlockRaw = GetBlockRaw BlockHash
 
+-- | Request for the best block: @GET \/block\/best@.
 newtype GetBlockBest = GetBlockBest NoTx
 
+-- | Request for the best block, serialized: @GET \/block\/best\/raw@.
 data GetBlockBestRaw = GetBlockBestRaw
 
+-- | Request for the best block and its ancestors, up to 100 blocks: @GET \/block\/latest@.
 newtype GetBlockLatest = GetBlockLatest NoTx
 
+-- | Request for the blocks at a height: @GET \/block\/height\/:height@.
 data GetBlockHeight = GetBlockHeight !HeightParam !NoTx
 
+-- | Request for the blocks at several heights: @GET \/block\/heights?heights=…@.
 data GetBlockHeights = GetBlockHeights !HeightsParam !NoTx
 
+-- | Request for the blocks at a height, serialized: @GET \/block\/height\/:height\/raw@.
 newtype GetBlockHeightRaw = GetBlockHeightRaw HeightParam
 
+-- | Request for the last block with a timestamp at or before a time: @GET \/block\/time\/:time@.
 data GetBlockTime = GetBlockTime !TimeParam !NoTx
 
+-- | Request for the last block with a timestamp at or before a time, serialized: @GET \/block\/time\/:time\/raw@.
 newtype GetBlockTimeRaw = GetBlockTimeRaw TimeParam
 
+-- | Request for the first block with a median time past at or after a time: @GET \/block\/mtp\/:time@.
 data GetBlockMTP = GetBlockMTP !TimeParam !NoTx
 
+-- | Request for the first block with a median time past at or after a time, serialized: @GET \/block\/mtp\/:time\/raw@.
 newtype GetBlockMTPRaw = GetBlockMTPRaw TimeParam
 
 -- Transactions
+-- | Request for a transaction: @GET \/transaction\/:txid@.
 newtype GetTx = GetTx TxHash
 
+-- | Request for several transactions: @GET \/transactions?txids=…@.
 newtype GetTxs = GetTxs [TxHash]
 
+-- | Request for a transaction, serialized: @GET \/transaction\/:txid\/raw@.
 newtype GetTxRaw = GetTxRaw TxHash
 
+-- | Request for several transactions, serialized: @GET \/transactions\/raw?txids=…@.
 newtype GetTxsRaw = GetTxsRaw [TxHash]
 
+-- | Request for the transactions of a block: @GET \/transactions\/block\/:block@.
 newtype GetTxsBlock = GetTxsBlock BlockHash
 
+-- | Request for the transactions of a block, serialized: @GET \/transactions\/block\/:block\/raw@.
 newtype GetTxsBlockRaw = GetTxsBlockRaw BlockHash
 
+-- | Request for whether any ancestor of a transaction is a coinbase confirmed after a height, or null if it cannot be computed within limits: @GET \/transaction\/:txid\/after\/:height@.
 data GetTxAfter = GetTxAfter !TxHash !HeightParam
 
+-- | Request to broadcast a transaction: @POST \/transactions@.
 newtype PostTx = PostTx Tx
 
+-- | Request for hashes of mempool transactions, newest first: @GET \/mempool@.
 data GetMempool = GetMempool !(Maybe LimitParam) !OffsetParam
 
+-- | Request for stream of new blocks and mempool transactions: @GET \/events@.
 data GetEvents = GetEvents
 
 -- Address
+-- | Request for transactions of an address: @GET \/address\/:address\/transactions@.
 data GetAddrTxs = GetAddrTxs !Address !LimitsParam
 
+-- | Request for transactions of several addresses: @GET \/address\/transactions?addresses=…@.
 data GetAddrsTxs = GetAddrsTxs ![Address] !LimitsParam
 
+-- | Request for full transactions of an address: @GET \/address\/:address\/transactions\/full@.
 data GetAddrTxsFull = GetAddrTxsFull !Address !LimitsParam
 
+-- | Request for full transactions of several addresses: @GET \/address\/transactions\/full?addresses=…@.
 data GetAddrsTxsFull = GetAddrsTxsFull ![Address] !LimitsParam
 
+-- | Request for balance of an address: @GET \/address\/:address\/balance@.
 newtype GetAddrBalance = GetAddrBalance Address
 
+-- | Request for balances of several addresses: @GET \/address\/balances?addresses=…@.
 newtype GetAddrsBalance = GetAddrsBalance [Address]
 
+-- | Request for unspent outputs of an address: @GET \/address\/:address\/unspent@.
 data GetAddrUnspent = GetAddrUnspent !Address !LimitsParam
 
+-- | Request for unspent outputs of several addresses: @GET \/address\/unspent?addresses=…@.
 data GetAddrsUnspent = GetAddrsUnspent ![Address] !LimitsParam
 
 -- XPubs
+-- | Request for summary of an extended public key: @GET \/xpub\/:xpub@.
 data GetXPub = GetXPub !XPubKey !Store.DeriveType !NoCache
 
+-- | Request for transactions of an extended public key: @GET \/xpub\/:xpub\/transactions@.
 data GetXPubTxs = GetXPubTxs !XPubKey !Store.DeriveType !LimitsParam !NoCache
 
+-- | Request for full transactions of an extended public key: @GET \/xpub\/:xpub\/transactions\/full@.
 data GetXPubTxsFull = GetXPubTxsFull !XPubKey !Store.DeriveType !LimitsParam !NoCache
 
+-- | Request for balances of the used addresses of an extended public key: @GET \/xpub\/:xpub\/balances@.
 data GetXPubBalances = GetXPubBalances !XPubKey !Store.DeriveType !NoCache
 
+-- | Request for unspent outputs of an extended public key: @GET \/xpub\/:xpub\/unspent@.
 data GetXPubUnspent = GetXPubUnspent !XPubKey !Store.DeriveType !LimitsParam !NoCache
 
+-- | Request to remove an extended public key from the cache: @DELETE \/xpub\/:xpub@.
 data DelCachedXPub = DelCachedXPub !XPubKey !Store.DeriveType
 
 -- Network
+-- | Request for connected peers: @GET \/peers@.
 data GetPeers = GetPeers
 
+-- | Request for health of the store: @GET \/health@.
 data GetHealth = GetHealth
 
 ------------
@@ -365,30 +438,38 @@
 -- Helpers --
 -------------
 
+-- | Path made of a prefix followed by one parameter.
 (<:>) :: Text -> [Text] -> Text
 (<:>) = (<+> "")
 
+-- | Path made of a prefix, one parameter and a suffix.
 (<+>) :: Text -> Text -> [Text] -> Text
 a <+> b = fill 1 [a, b]
 
+-- | Path made of a prefix, a parameter, an infix and another parameter.
 (<++>) :: Text -> Text -> [Text] -> Text
 a <++> b = fill 2 [a, b]
 
+-- | Interleave path fragments with this many parameters. Calls 'error' if the number of parameters is wrong.
 fill :: Int -> [Text] -> [Text] -> Text
 fill i a b
   | length b /= i = error "Invalid query parameters"
   | otherwise = mconcat $ uncurry (<>) <$> zip a (b <> repeat "")
 
+-- | Parameter to send, unless it has its default value.
 noDefBox :: (Default p, Param p, Eq p) => p -> [ParamBox]
 noDefBox p = [ParamBox p | p /= def]
 
+-- | Parameter to send, if any.
 noMaybeBox :: (Param p, Eq p) => Maybe p -> [ParamBox]
 noMaybeBox (Just p) = [ParamBox p]
 noMaybeBox _ = []
 
+-- | Proxy for the type of a value.
 asProxy :: a -> Proxy a
 asProxy = const Proxy
 
+-- | Path of a request, with its path parameters filled in.
 queryPath :: (ApiResource a b) => Network -> Ctx -> a -> Text
 queryPath net ctx a = f $ encParam <$> fst (queryParams a)
   where
@@ -398,6 +479,7 @@
         Just [res] -> res
         _ -> error "Invalid query param"
 
+-- | Route pattern of an endpoint for the server, with a named capture for each path parameter.
 capturePath :: (ApiResource a b) => Proxy a -> Scotty.RoutePattern
 capturePath proxy =
   fromString $ cs $ f $ toLabel <$> captureParams proxy
@@ -405,6 +487,7 @@
     f = resourcePath proxy
     toLabel (ProxyBox p) = ":" <> proxyLabel p
 
+-- | Name of a parameter in URLs.
 paramLabel :: (Param p) => p -> Text
 paramLabel = proxyLabel . asProxy
 
@@ -412,9 +495,14 @@
 -- Options --
 -------------
 
+-- | Type that can be a parameter of a request.
 class Param a where
+  -- | Name of the parameter in URLs.
   proxyLabel :: Proxy a -> Text
+  -- | Encode a value as one or more strings. A list parameter is sent as
+  -- comma-separated values.
   encodeParam :: Network -> Ctx -> a -> Maybe [Text]
+  -- | Parse a value from one or more strings.
   parseParam :: Network -> Ctx -> [Text] -> Maybe a
 
 instance Param Address where
@@ -428,10 +516,16 @@
   encodeParam net ctx = mapM (addrToText net)
   parseParam net ctx = mapM (textToAddr net)
 
+-- | Where a page of results starts. In URLs it is a transaction hash, a block
+-- height, or a Unix timestamp. Numbers up to 1230768000 are heights and larger
+-- ones are timestamps.
 data StartParam
-  = StartParamHash {hash :: Hash256}
-  | StartParamHeight {height :: Natural}
-  | StartParamTime {time :: Store.UnixTime}
+  = -- | start at a transaction
+    StartParamHash {hash :: Hash256}
+  | -- | start at a block height
+    StartParamHeight {height :: Natural}
+  | -- | start at the last block at or before a time
+    StartParamTime {time :: Store.UnixTime}
   deriving (Eq, Show)
 
 instance Param StartParam where
@@ -462,6 +556,7 @@
         return $ StartParamTime x
   parseParam net ctx _ = Nothing
 
+-- | Number of results to skip (@offset@).
 newtype OffsetParam = OffsetParam {get :: Natural}
   deriving (Eq, Show, Read, Enum, Ord, Num, Real, Integral)
 
@@ -474,6 +569,7 @@
   parseParam net ctx [s] = OffsetParam <$> readMaybe (cs s)
   parseParam net ctx _ = Nothing
 
+-- | Maximum number of results (@limit@).
 newtype LimitParam = LimitParam {get :: Natural}
   deriving (Eq, Show, Read, Enum, Ord, Num, Real, Integral)
 
@@ -483,9 +579,14 @@
   parseParam net ctx [s] = LimitParam <$> readMaybe (cs s)
   parseParam net ctx _ = Nothing
 
+-- | Pagination of a list of results.
 data LimitsParam = LimitsParam
-  { limit :: Maybe LimitParam, -- 0 means maximum
+  { -- | maximum number of results; @0@ means the maximum the server allows,
+    -- and 'Nothing' the server default
+    limit :: Maybe LimitParam,
+    -- | number of results to skip after the start
     offset :: OffsetParam,
+    -- | where to start, or from the newest result
     start :: Maybe StartParam
   }
   deriving (Eq, Show)
@@ -493,6 +594,7 @@
 instance Default LimitsParam where
   def = LimitsParam Nothing def Nothing
 
+-- | Block height (@height@).
 newtype HeightParam = HeightParam {get :: Natural}
   deriving (Eq, Show, Read, Enum, Ord, Num, Real, Integral)
 
@@ -502,6 +604,7 @@
   parseParam net ctx [s] = HeightParam <$> readMaybe (cs s)
   parseParam net ctx _ = Nothing
 
+-- | List of block heights (@heights@).
 newtype HeightsParam = HeightsParam {get :: [Natural]}
   deriving (Eq, Show, Read)
 
@@ -510,6 +613,7 @@
   encodeParam net ctx (HeightsParam hs) = Just $ cs . show <$> hs
   parseParam net ctx xs = HeightsParam <$> mapM (readMaybe . cs) xs
 
+-- | Unix timestamp in seconds (@time@).
 newtype TimeParam = TimeParam {get :: Store.UnixTime}
   deriving (Eq, Show, Read, Enum, Ord, Num, Real, Integral)
 
@@ -537,6 +641,7 @@
     guard (net.segWit || res == Store.DeriveNormal)
     return res
 
+-- | Do not use the cache to answer an xpub query (@nocache@).
 newtype NoCache = NoCache {get :: Bool}
   deriving (Eq, Show, Read)
 
@@ -552,6 +657,8 @@
     ["false"] -> Just $ NoCache False
     _ -> Nothing
 
+-- | Leave out the transaction hashes of a block, except that of the coinbase
+-- (@notx@).
 newtype NoTx = NoTx {get :: Bool}
   deriving (Eq, Show, Read)
 
