diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Author name here (c) 2017
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Author name here nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+## The Airtable API for Haskell
+
+Provides a high-level interface to requesting and introspecting Tables within an Airtable project. 
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/airtable-api.cabal b/airtable-api.cabal
new file mode 100644
--- /dev/null
+++ b/airtable-api.cabal
@@ -0,0 +1,41 @@
+name:                airtable-api
+version:             0.1.0.0
+synopsis:            Requesting and introspecting Tables within an Airtable project. 
+-- description:
+homepage:            https://github.com/ooblahman/airtable-api
+license:             BSD3
+license-file:        LICENSE
+author:              Anand Srinivasan
+maintainer:          Anand Srinivasan
+copyright:           AlphaSheets, Inc
+category:            Web
+build-type:          Simple
+extra-source-files:  README.md
+cabal-version:       >=1.10
+
+library
+  hs-source-dirs:      src
+  exposed-modules:     Airtable.Table
+                     , Airtable.Query
+  build-depends:       base >= 4.7 && < 5
+                     , text
+                     , bytestring
+                     , aeson
+                     , wreq
+                     , hashable
+                     , lens
+                     , unordered-containers
+  default-language:    Haskell2010
+
+test-suite airtable-api-test
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Spec.hs
+  build-depends:       base
+                     , airtable-api
+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
+  default-language:    Haskell2010
+
+source-repository head
+  type:     git
+  location: https://github.com/githubuser/airtable-api
diff --git a/src/Airtable/Query.hs b/src/Airtable/Query.hs
new file mode 100644
--- /dev/null
+++ b/src/Airtable/Query.hs
@@ -0,0 +1,59 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Airtable.Query
+    ( module Airtable.Table
+    , AirtableOptions
+    , getTable
+    ) where
+
+
+import           Airtable.Table
+
+import           Network.Wreq
+
+import           Control.Lens ((^.), (.~), (&))
+
+import           Data.Aeson (FromJSON, ToJSON, eitherDecode)
+import           Data.Monoid
+import qualified Data.ByteString.Char8 as BC
+
+-- * Configuration for Airtable requests.
+
+data AirtableOptions = AirtableOptions {
+    apiKey :: String
+  , appId :: String  
+  }
+
+-- * Constants
+
+-- | Base airtable API string. 
+base_url :: String
+base_url = "https://api.airtable.com/v0/"
+
+-- * Main API
+
+-- | Retrieve a table from airtable.com given its name. 
+getTable :: (FromJSON a) => AirtableOptions -> TableName -> IO (Table a)
+getTable opts tname = getTableFromUrl net_otps url 
+  where
+    net_otps = defaults & header "Authorization" .~ ["Bearer " <> BC.pack (apiKey opts)] 
+    url  = base_url <> appId opts <> "/" <> tname
+
+getTableFromUrl :: (FromJSON a) => Options -> String -> IO (Table a)
+getTableFromUrl opts url = do
+  resp <- getWith opts url
+  getMore (fromResp resp)
+  where
+    getMore tbl = case tableOffset tbl of 
+      Just offset -> do
+        resp <- getWith (opts & param "offset" .~ [offset]) url
+        putStrLn $ "Part " <> show offset
+        getMore $ fromResp resp <> tbl
+      Nothing -> 
+        pure tbl
+
+    fromResp r = decoder $ r ^. responseBody
+      where
+        decoder b = case eitherDecode b of 
+          Left e -> error $ e <> "\nSource string: " <> show b
+          Right r -> r
diff --git a/src/Airtable/Table.hs b/src/Airtable/Table.hs
new file mode 100644
--- /dev/null
+++ b/src/Airtable/Table.hs
@@ -0,0 +1,131 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+module Airtable.Table
+    ( RecordID(..)
+    , rec2str
+    , IsRecord(..)
+    , Table(..)
+    , TableName
+    , toList
+    , exists
+    , select
+    , selectMaybe
+    , selectAll
+    , selectAllKeys
+    , selectWhere
+    , selectKeyWhere
+    , deleteWhere
+    ) where
+
+
+import           GHC.Generics
+import           GHC.Stack
+
+import           Control.Applicative ((<|>))
+
+import           Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as Map
+import           Data.Aeson
+import           Data.Aeson.Types
+import           Data.Text (Text)
+import qualified Data.Text as T
+import           Data.Monoid
+import           Data.Hashable
+import           Data.Foldable (foldlM)
+
+
+-- * RecordID 
+
+-- | Airtable's record ID for use in indexing records
+newtype RecordID = RecordID Text deriving (FromJSON, Show, Eq, Generic, Ord)
+
+instance Hashable RecordID 
+
+rec2str :: RecordID -> String
+rec2str (RecordID rec) = T.unpack rec
+
+-- * IsRecord class
+
+-- | A convenience typeclass for selecting records using RecordID-like keys.
+class IsRecord a where
+  toRec :: a -> RecordID
+
+instance IsRecord RecordID where
+  toRec = id
+
+instance IsRecord String where
+  toRec = RecordID . T.pack 
+
+-- * Table 
+
+-- | Airtable's table type
+data Table a = Table { tableRecords :: Map.HashMap RecordID a
+                     , tableOffset :: Maybe Text
+                     } deriving 
+                     ( Show )
+
+-- | Synonym used in querying tables from the API.
+type TableName = String
+
+instance (FromJSON a) => FromJSON (Table a) where
+  parseJSON (Object v) = do
+    recs <- v .: "records" :: Parser [Value]
+    parsedRecs <- foldlM parseRec Map.empty recs
+    offset <- v .:? "offset"
+    return $ Table parsedRecs offset
+    where
+      parseRec tbl (Object v) = 
+            do  recId <- v .: "id"
+                obj <- v .: "fields" 
+                return $ Map.insert recId obj tbl
+        <|> error ("could not decode: " <> show v)
+
+instance Monoid (Table a) where
+  mempty = Table mempty Nothing
+  mappend (Table t1 o) (Table t2 _) = Table (mappend t1 t2) o
+
+-- * Table methods
+
+-- | Convert a 'Table' to a list of key-record pairs.
+toList :: Table a -> [(RecordID, a)]
+toList = Map.toList . tableRecords
+
+-- | Check if a record exists at the given key in a table.
+exists :: (IsRecord r) => Table a -> r -> Bool
+exists tbl rec = Map.member (toRec rec) (tableRecords tbl)
+
+-- | Unsafely lookup a record using its RecordID.
+select :: (HasCallStack, IsRecord r, Show a) => Table a -> r -> a
+select tbl rec = tableRecords tbl `lookup` toRec rec
+  where
+    lookup mp k = case Map.lookup k mp of 
+      Just v -> v
+      Nothing -> error $ "lookup failed in map: " <> show k
+
+-- | Safely lookup a record using its RecordID.
+selectMaybe :: (IsRecord r, Show a) => Table a -> r -> Maybe a
+selectMaybe tbl rec = toRec rec `Map.lookup` tableRecords tbl 
+
+-- | Read all records.
+selectAll :: Table a -> [a]
+selectAll = map snd . toList
+
+-- | Read all RecordID's.
+selectAllKeys :: Table a -> [RecordID]
+selectAllKeys = map fst . toList
+
+-- | Select all records satisfying a condition. 
+selectWhere :: Table a -> (RecordID -> a -> Bool) -> [a]
+selectWhere tbl f = map snd $ filter (uncurry f) (toList tbl)
+
+-- | Select all RecordID's satisfying a condition.
+selectKeyWhere :: Table a -> (RecordID -> a -> Bool) -> [RecordID]
+selectKeyWhere tbl f = map fst $ filter (uncurry f) (toList tbl)
+
+-- | Delete all Records satisfying a condition. 
+deleteWhere :: Table a -> (RecordID -> a -> Bool) -> Table a
+deleteWhere (Table recs off) f = Table (Map.filterWithKey (\k v -> not $ f k v) recs) off
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,2 @@
+main :: IO ()
+main = putStrLn "Test suite not yet implemented"
