diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@
   manager <- newManager defaultManagerSettings
   response <- getDocType manager config "Company A" :: IO (ApiResponse Customer)
   case response of
-    Ok _ c _ -> putStrLn $ name c
+    Ok _ _ c -> putStrLn $ name c
     Err r _ -> print r
 ```
 
@@ -103,9 +103,23 @@
 dependency. The resulting record types can be used together with this
 API client but the `IsDocType` instance must still be defined by hand.
 
-****The API client part generated from the OpenAPI spec can not be
-used.****
+**The API client part generated from the OpenAPI spec can not be used.**
 
+Example models file:
+
+``` plain
+SalesOrder
+  name Text
+  total Double
+  transaction_date Text
+  items [SalesOrderItem]
+  Required name
+
+SalesOrderItem
+  name Text
+  Required name
+```
+
 ``` example
 $ ./scripts/gen-openapi-yaml.sh models > openapi.yaml
 $ openapi3-code-generator-exe \
@@ -125,13 +139,11 @@
 │   │   ├── SecuritySchemes.hs
 │   │   ├── TypeAlias.hs
 │   │   ├── Types     <---- here are the generated types
-│   │   │   ├── Customer.hs
-│   │   │   ├── Order.hs
-│   │   │   ├── Supplier.hs
+│   │   │   ├── SalesOrder.hs
+│   │   │   ├── SalesOrderItem.hs
 │   │   └── Types.hs
 │   └── ERPNextAPI.hs
-├── stack.yaml
-└── stack.yaml.lock
+└── stack.yaml
 ```
 
 ## Note on TLS problems
diff --git a/Setup.hs b/Setup.hs
deleted file mode 100644
--- a/Setup.hs
+++ /dev/null
@@ -1,2 +0,0 @@
-import Distribution.Simple
-main = defaultMain
diff --git a/erpnext-api-client.cabal b/erpnext-api-client.cabal
--- a/erpnext-api-client.cabal
+++ b/erpnext-api-client.cabal
@@ -1,7 +1,7 @@
 cabal-version:       3.6
 
 name:                erpnext-api-client
-version:             0.1.0.0
+version:             0.1.0.1
 synopsis:            Generic API client library for ERPNext
 description:         This is a Haskell API client for ERPNext. It aims to be a light-weight library based on http-client and user-provided record types.
 homepage:            https://github.com/schoettl/erpnext-api-client
diff --git a/src/ERPNext/Client.hs b/src/ERPNext/Client.hs
--- a/src/ERPNext/Client.hs
+++ b/src/ERPNext/Client.hs
@@ -2,6 +2,17 @@
 {-# LANGUAGE TypeApplications #-}
 {-# LANGUAGE OverloadedStrings #-}
 
+{-|
+Description: Generic API client library for ERPNext
+
+This is a Haskell API client for ERPNext. It aims to be a light-weight
+library based on http-client and user-provided record types.
+
+API documentation:
+
+https://docs.frappe.io/framework/user/en/api/rest
+-}
+
 module ERPNext.Client
   ( getDocTypeList
   , getDocType
@@ -29,11 +40,16 @@
 import ERPNext.Client.Helper (urlEncode)
 
 -- | Type class for types which represent an ERPNext DocType.
--- Each DocType has a unique name.
+-- Each DocType has a unique name but there can still be multiple
+-- „views“ (i.e. records types) for one DocType.
 class IsDocType a where
   docTypeName :: Text
-  -- TODO: implement auto-derive (using typename and generic)?
 
+{-|
+  Get a list of all documents of a given DocType.
+  The 'QueryStringParam's can select fields, filter, order, enable
+  paging, and more.
+-}
 getDocTypeList :: forall a. (IsDocType a, FromJSON a)
                => Manager -> Config  -> [QueryStringParam]-> IO (ApiResponse [a])
 getDocTypeList manager config qsParams = do
@@ -42,6 +58,7 @@
   response <- httpLbs request manager
   return $ parseGetResponse response
 
+-- | Get a single document of a given DocType by name.
 getDocType :: forall a. (IsDocType a, FromJSON a)
            => Manager -> Config -> Text -> IO (ApiResponse a)
 getDocType manager config name = do
@@ -50,13 +67,13 @@
   response <- httpLbs request manager
   return $ parseGetResponse response
 
-{- | Delete a named object.
+{- | Delete a single document of a given DocType by name.
 
 The phantom type parameter @a@ is used to figure out the DocType.
 A customer can be deleted like this:
 
 @
-res <- deleteDocType @Customer manager config "<customer name>"
+res \<- deleteDocType @Customer manager config "customer name"
 @
 -}
 deleteDocType :: forall a. (IsDocType a)
@@ -67,6 +84,7 @@
   response <- httpLbs request manager
   return $ parseDeleteResponse response
 
+-- | Create a new document of a given DocType.
 postDocType :: forall a. (IsDocType a, FromJSON a, ToJSON a)
             => Manager -> Config -> a -> IO (ApiResponse a)
 postDocType manager config doc = do
@@ -75,6 +93,7 @@
   response <- httpLbs request manager
   return $ parseGetResponse response
 
+-- | Update a document of a given DocType by name.
 putDocType :: forall a. (IsDocType a, FromJSON a, ToJSON a)
            => Manager -> Config -> Text -> a -> IO (ApiResponse a)
 putDocType manager config name doc = do
@@ -83,7 +102,7 @@
   response <- httpLbs request manager
   return $ parseGetResponse response
 
-
+-- | Create an API client configuration.
 mkConfig :: Text -> Text -> Secret -> Config
 mkConfig baseUrl apiKey apiSecret = Config
   { baseUrl = baseUrl
@@ -96,7 +115,7 @@
 mkSecret = Secret
 
 
--- | Create the API Request.
+-- | Create the API 'Request'.
 createRequest :: Config -> Text -> BS.ByteString -> IO Request
 createRequest config path method = do
   request <- parseRequest $ unpack (baseUrl config <> path)
@@ -105,6 +124,7 @@
     , requestHeaders = [mkAuthHeader config]
     }
 
+-- | Create the API 'Request' with a JSON body.
 createRequestWithBody :: ToJSON a => Config -> Text -> BS.ByteString -> a -> IO Request
 createRequestWithBody config path method doc = do
   request <- parseRequest $ unpack (baseUrl config <> path)
@@ -126,6 +146,7 @@
   { getSecret :: Text
   }
 
+-- | Data wrapper type just to parse the JSON returned by ERPNext.
 data DataWrapper a = DataWrapper { getData :: a }
   deriving Show
 
@@ -134,11 +155,20 @@
     dataValue <- obj .: "data"
     return (DataWrapper dataValue)
 
+-- | The API response.
 data ApiResponse a
-  = Ok (Response LBS.ByteString) Value a
-  | Err (Response LBS.ByteString) (Maybe (Value, Text))
+  = Ok -- ^ The OK response.
+      (Response LBS.ByteString) -- ^ The server's full response including header information.
+      Value -- ^ The returned JSON.
+      a -- ^ The result parsed from the returned JSON.
+  | Err -- ^ The error response.
+      (Response LBS.ByteString) -- ^ The server's full response including header information.
+      (Maybe (Value, Text)) -- ^ If the response is valid JSON, 'Just' the returned JSON and
+                            -- the parse error message telling why 'Value' couldn't be parsed
+                            -- into @a@.
   deriving Show
 
+-- | Get the full response from the API response.
 getResponse :: ApiResponse a -> Response LBS.ByteString
 getResponse (Ok r _ _) = r
 getResponse (Err r _) = r
diff --git a/src/ERPNext/Client/Filters.hs b/src/ERPNext/Client/Filters.hs
--- a/src/ERPNext/Client/Filters.hs
+++ b/src/ERPNext/Client/Filters.hs
@@ -81,6 +81,7 @@
     <> renderFilterValue (filterValue f)
     <> "]"
 
+-- | Render the filter terms for the URL query string.
 renderFilters :: Text -> [Filter] -> Text
 renderFilters prefix filters =
   let
diff --git a/src/ERPNext/Client/Helper.hs b/src/ERPNext/Client/Helper.hs
--- a/src/ERPNext/Client/Helper.hs
+++ b/src/ERPNext/Client/Helper.hs
@@ -10,15 +10,19 @@
 import Data.Text qualified as T
 import Network.URI (escapeURIString, isUnreserved)
 
+-- | Percent-encode string for use in a URL.
 urlEncode :: Text -> Text
 urlEncode = pack . escapeURIString isUnreserved . unpack
 
 sanitizeQuotes :: Text -> Text
 sanitizeQuotes = T.filter (/= '"')
 
+-- TODO: is it a good idea to just drop "?
+-- | Double-quote string after dropping existing double quotes.
 quote :: Text -> Text
 quote t = "\"" <> sanitizeQuotes t <> "\""
 
+-- | 'show' but return 'Text'.
 tshow :: Show a => a -> Text
 tshow = pack . show
 
diff --git a/src/ERPNext/Client/QueryStringParams.hs b/src/ERPNext/Client/QueryStringParams.hs
--- a/src/ERPNext/Client/QueryStringParams.hs
+++ b/src/ERPNext/Client/QueryStringParams.hs
@@ -14,22 +14,22 @@
 -- TODO: Maybe change type or make opaque type to prevent invalid combinations?
 -- TODO: add variants for limit, offset, etc.?
 data QueryStringParam
-  = Debug Bool -- ^ if 'True', makes API returning query analysis info instead of data
-  | AsDict Bool -- ^ if 'False', makes API returning the data records as mixed-type arrays which cannot be parsed by this library (default: 'True')
-  | LimitStart Int -- ^ offset
-  | LimitPageLength Int -- ^ page size
-  | Asc Text
-  | Desc Text
-  | Fields [Text]
-  | AndFilter [Filter]
-  | OrFilter [Filter]
+  = Debug Bool -- ^ If 'True', makes API returning query analysis info instead of data
+  | AsDict Bool -- ^ If 'False', makes API returning the data records as mixed-type arrays which cannot be parsed by this library (default: 'True')
+  | LimitStart Int -- ^ Page offset (starts at 0)
+  | LimitPageLength Int -- ^ Page size
+  | Asc Text -- ^ Ascending order by given field
+  | Desc Text -- ^ Descending order by given field
+  | Fields [Text] -- ^ Select fields
+  | AndFilter [Filter] -- ^ Filter terms combined with logical AND
+  | OrFilter [Filter] -- ^ Filter terms combined with logical OR
 
 renderQueryStringParam :: QueryStringParam -> Text
 renderQueryStringParam qsParam =
   case qsParam of
     Debug b -> "debug=" <> tshow b
     AsDict b -> "as_dict=" <> tshow b
-    LimitStart offset -> "limit_start=" <> tshow offset
+    LimitStart offset -> "limit_start=" <> tshow (max 0 offset)
     LimitPageLength n -> "limit=" <> tshow n -- it was limit_page_length up to v13
 
     Asc field ->
@@ -55,5 +55,6 @@
 renderOrderBy field order =
   "order_by=" <> urlEncode (field <> " " <> order)
 
+-- | Render the query string for the URL.
 renderQueryStringParams :: [QueryStringParam] -> Text
 renderQueryStringParams params = intercalate "&" (map renderQueryStringParam params)
