diff --git a/airtable-api.cabal b/airtable-api.cabal
--- a/airtable-api.cabal
+++ b/airtable-api.cabal
@@ -1,5 +1,5 @@
 name:                airtable-api
-version:             0.2.0.1
+version:             0.2.1.1
 synopsis:            Requesting and introspecting Tables within an Airtable project.
 -- description:
 homepage:            https://github.com/ooblahman/airtable-api
diff --git a/src/Airtable/Query.hs b/src/Airtable/Query.hs
--- a/src/Airtable/Query.hs
+++ b/src/Airtable/Query.hs
@@ -5,21 +5,31 @@
       -- * Configuration for Airtable requests.
     , AirtableOptions(..)
     , defaultAirtableOptions
-      -- * Main API
-    , getTable
+    , airtableOptionsToWreqOptions
+    , tableNameToUrl
+      -- * API
+    , getRecords
+    , createRecord
+    , updateRecord
+    , deleteRecord
     ) where
 
 
 import           Airtable.Table
 
+import           GHC.Stack
 import           Network.Wreq
 
 import           Control.Lens ((^.), (.~), (&))
+import           Control.Monad (void)
 
-import           Data.Aeson (FromJSON, ToJSON, eitherDecode)
+import           Data.Aeson (FromJSON, ToJSON, eitherDecode, toJSON)
 import           Data.Monoid
+import           Data.ByteString.Lazy (ByteString)
 import qualified Data.ByteString.Char8 as BC
 
+-- * Configuration for Airtable requests.
+
 -- | Options
 data AirtableOptions = AirtableOptions {
   -- | the API key for your project
@@ -43,21 +53,39 @@
 base_url :: String
 base_url = "https://api.airtable.com/"
 
--- | Retrieve a table from airtable.com given its name. Handles pagination correctly.
-getTable :: (FromJSON a) => AirtableOptions -> TableName -> IO (Table a)
-getTable opts tname = getTableFromUrl net_otps url
+-- | Produce Wreq options from 'AirtableOptions'
+airtableOptionsToWreqOptions :: AirtableOptions -> Options
+airtableOptionsToWreqOptions opts = 
+  defaults & header "Authorization" .~ ["Bearer " <> BC.pack (apiKey opts)]
+
+-- | Produce a request URL to a table.
+tableNameToUrl :: AirtableOptions -> TableName -> String
+tableNameToUrl opts tname = 
+     base_url
+  <> "v"
+  <> show (apiVersion opts)
+  <> "/"
+  <> appId opts
+  <> "/"
+  <> tname
+
+-- * API
+
+fromResp :: (HasCallStack, FromJSON a) => Response ByteString -> a
+fromResp r = decoder $ r ^. responseBody
   where
-    net_otps = defaults & header "Authorization" .~ ["Bearer " <> BC.pack (apiKey opts)]
-    url  =   base_url
-          <> "v"
-          <> show (apiVersion opts)
-          <> "/"
-          <> appId opts
-          <> "/"
-          <> tname
+    decoder b = case eitherDecode b of
+      Left e  -> error e
+      Right r -> r
 
-getTableFromUrl :: (FromJSON a) => Options -> String -> IO (Table a)
-getTableFromUrl opts url = do
+-- | Retrieve the records for a table from airtable.com given its name. Handles pagination correctly.
+getRecords :: (HasCallStack, FromJSON a) => AirtableOptions -> TableName -> IO (Table a)
+getRecords opts tname = 
+  getRecordsFromUrl (airtableOptionsToWreqOptions opts) (tableNameToUrl opts tname)
+
+-- | Retrieve the records for a table given a URL and network options.
+getRecordsFromUrl :: (HasCallStack, FromJSON a) => Options -> String -> IO (Table a)
+getRecordsFromUrl opts url = do
   resp <- getWith opts url
   getMore (fromResp resp)
   where
@@ -68,8 +96,31 @@
       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
+-- | Upload a record to a given table. Returns the newly created `RecordID`.
+createRecord :: (HasCallStack, ToJSON a, FromJSON a) => AirtableOptions -> TableName -> a -> IO (Record a)
+createRecord opts tname a = do
+  resp <- postWith netOpts url payload
+  return $ fromResp resp
+  where
+    url = tableNameToUrl opts tname
+    netOpts = airtableOptionsToWreqOptions opts 
+    payload = toJSON a
+
+-- | Update a record on a given table, using the supplied fields ('a').
+updateRecord :: (ToJSON a) => AirtableOptions -> TableName -> RecordID -> a -> IO ()
+updateRecord opts tname recId a = do
+  void $ 
+    customPayloadMethodWith "PATCH" netOpts url payload
+  where
+    url = tableNameToUrl opts tname <> "/" <> rec2str recId
+    netOpts = airtableOptionsToWreqOptions opts
+    payload = toJSON a
+
+-- | Delete a record on a table. 
+deleteRecord :: AirtableOptions -> TableName -> RecordID -> IO ()
+deleteRecord opts tname recId = 
+  void $ deleteWith netOpts url 
+  where
+    netOpts = airtableOptionsToWreqOptions opts
+    url = tableNameToUrl opts tname <> "/" <> rec2str recId
+
diff --git a/src/Airtable/Table.hs b/src/Airtable/Table.hs
--- a/src/Airtable/Table.hs
+++ b/src/Airtable/Table.hs
@@ -7,8 +7,10 @@
 
 module Airtable.Table
     (
+    -- * Record
+      Record(..)
     -- * RecordID
-      RecordID(..)
+    , RecordID(..)
     , rec2str
     -- * IsRecord class
     , IsRecord(..)
@@ -67,16 +69,28 @@
 
 -- | A convenience typeclass for selecting records using RecordID-like keys.
 class IsRecord a where
-  toRec :: a -> RecordID
+  toRecId :: a -> RecordID
 
 instance IsRecord RecordID where
-  toRec = id
+  toRecId = id
 
 instance IsRecord String where
-  toRec = RecordID . T.pack
+  toRecId = RecordID . T.pack
 
 -- * Table
 
+-- | An airtable record.
+data Record a = Record { recordId :: RecordID, recordObj :: a, createdTime :: UTCTime }
+
+instance (FromJSON a) => FromJSON (Record a) where
+  parseJSON = withObject "record object" $ \v -> do
+    Record <$> v .: "id" 
+           <*> v .: "fields"
+           <*> v .: "createdTime"
+
+instance IsRecord (Record a) where
+  toRecId = recordId
+
 -- | Airtable's table type
 data Table a = Table { tableRecords :: Map.HashMap RecordID a
                      , tableOffset :: Maybe Text
@@ -92,27 +106,24 @@
 type TableName = String
 
 instance (FromJSON a) => FromJSON (Table a) where
-  parseJSON (Object v) = do
+  parseJSON = withObject "table object" $ \v -> do
     recs <- v .: "records" :: Parser [Value]
     parsedRecs <- foldlM parseRec Map.empty recs
     offset <- v .:? "offset"
     return $ Table parsedRecs offset
     where
-      parseRec tbl json@(Object v) =
+      parseRec tbl = withObject "record object" $ \v -> 
             do  recId <- v .: "id"
                 fields <- v .: "fields"
                 obj <- parseJSON fields
                 return $ Map.insert recId obj tbl
-      parseRec tbl invalid = typeMismatch "Table" invalid
-  parseJSON invalid = typeMismatch "Table" invalid
 
 parseRecord :: (UTCTime -> RecordID -> Value -> Parser a) -> Value -> Parser a
-parseRecord action (Object v) = do
+parseRecord action = withObject "record object" $ \v -> do
     created  <- v .: "createdTime"
     recordId <- v .: "id"
     fields   <- v .: "fields"
     action created recordId fields
-parseRecord _action invalid = typeMismatch "parseFields" invalid
 
 parseFields :: (Value -> Parser a) -> Value -> Parser a
 parseFields action (Object v) = v .: "fields" >>= action
@@ -130,12 +141,12 @@
 
 -- | 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)
+exists tbl rec = Map.member (toRecId rec) (tableRecords tbl)
 
 -- | Unsafely lookup a record using its RecordID. Will throw a pretty-printed error
 --   if record does not exist.
 select :: (HasCallStack, IsRecord r, Show a) => Table a -> r -> a
-select tbl rec = tableRecords tbl `lookup` toRec rec
+select tbl rec = tableRecords tbl `lookup` toRecId rec
   where
     lookup mp k = case Map.lookup k mp of
       Just v -> v
@@ -143,7 +154,7 @@
 
 -- | Safely lookup a record using its RecordID.
 selectMaybe :: (IsRecord r) => Table a -> r -> Maybe a
-selectMaybe tbl rec = toRec rec `Map.lookup` tableRecords tbl
+selectMaybe tbl rec = toRecId rec `Map.lookup` tableRecords tbl
 
 -- | Read all records.
 selectAll :: Table a -> [a]
