diff --git a/2captcha.cabal b/2captcha.cabal
new file mode 100644
--- /dev/null
+++ b/2captcha.cabal
@@ -0,0 +1,69 @@
+cabal-version:      1.12
+name:               2captcha
+version:            0.1.0.0
+license:            MIT
+license-file:       LICENSE
+copyright:          2021 Edward Yang
+maintainer:         edwardyang0410@gmail.com
+author:             Edward Yang
+homepage:           https://github.com/qwbarch/2captcha-haskell#readme
+bug-reports:        https://github.com/qwbarch/2captcha-haskell/issues
+synopsis:           Haskell package for easy integration with the 2captcha API.
+description:
+    Haskell package for easy integration with the 2captcha API.
+    .
+    Feature list:
+    .
+    * Lens-based API
+    * Uses Wreq for http calls
+
+category:           Network
+build-type:         Simple
+extra-source-files:
+    README.md
+    CHANGELOG.md
+
+source-repository head
+    type:     git
+    location: https://github.com/qwbarch/2captcha-haskell
+
+library
+    exposed-modules:
+        TwoCaptcha.Captcha
+        TwoCaptcha.Internal.Client
+        TwoCaptcha.Internal.Types.Captcha
+        TwoCaptcha.Internal.Types.CapyCaptcha
+        TwoCaptcha.Internal.Types.CoordinateCaptcha
+        TwoCaptcha.Internal.Types.Exception
+        TwoCaptcha.Internal.Types.FunCaptcha
+        TwoCaptcha.Internal.Types.GeeTestCaptcha
+        TwoCaptcha.Internal.Types.GridCaptcha
+        TwoCaptcha.Internal.Types.HCaptcha
+        TwoCaptcha.Internal.Types.KeyCaptcha
+        TwoCaptcha.Internal.Types.NormalCaptcha
+        TwoCaptcha.Internal.Types.ReCaptcha
+        TwoCaptcha.Internal.Types.RotateCaptcha
+        TwoCaptcha.Internal.Types.TextCaptcha
+        TwoCaptcha.Internal.Types.TikTokCaptcha
+
+    hs-source-dirs:     src
+    other-modules:      Paths_2captcha
+    default-language:   Haskell2010
+    default-extensions:
+        OverloadedStrings FlexibleContexts FlexibleInstances
+        UndecidableInstances RankNTypes ScopedTypeVariables LambdaCase
+        MultiParamTypeClasses TemplateHaskell
+
+    ghc-options:        -Wall -Wno-name-shadowing
+    build-depends:
+        aeson >=1.5.6.0 && <1.6,
+        base >=4.7 && <5,
+        bytestring >=0.10.12.0 && <0.11,
+        clock >=0.8.2 && <0.9,
+        exceptions >=0.10.4 && <0.11,
+        http-client >=0.6.4.1 && <0.7,
+        lens >=4.19.2 && <4.20,
+        lens-aeson >=1.1.1 && <1.2,
+        parsec >=3.1.14.0 && <3.2,
+        text >=1.2.4.1 && <1.3,
+        wreq >=0.5.3.3 && <0.6
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,4 @@
+# Changelog for 2captcha-haskell
+
+## [0.1.0.0] - 2021-09-08
+- Initial release
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Edward Yang
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,233 @@
+# 2captcha-haskell
+
+The easiest way to integrate 2captcha into your application to automate common types of captchas.
+
+#### Table of Contents
+- [Solving a captcha](#solving-a-captcha)
+  - [Normal Captcha](#normal-captcha)
+  - [Text Captcha](#text-captcha)
+  - [ReCaptcha V2](#recaptcha-v2)
+  - [ReCaptcha V3](#recaptcha-v3)
+  - [FunCaptcha](#funcaptcha)
+  - [GeeTest](#geetest)
+  - [hCaptcha](#hcaptcha)
+  - [KeyCaptcha](#keycaptcha)
+  - [Capy](#capy)
+  - [Grid](#grid)
+  - [Coordinate](#coordinate)
+  - [Rotate](#rotate)
+  - [TikTok](#tiktok)
+
+## Solving a captcha
+
+Solving a captcha using 2captcha's api is through the ``in.php`` and ``res.php`` endpoints.  
+This package encapsulates the mentioned endpoints through [submit](https://github.com/qwbarch/2captcha-haskell/blob/e442950a79f1aef7fd2fb95aac9cd9bfe41a8df7/src/TwoCaptcha/Internal/Client.hs#L48) and [answer](https://github.com/qwbarch/2captcha-haskell/blob/e442950a79f1aef7fd2fb95aac9cd9bfe41a8df7/src/TwoCaptcha/Internal/Client.hs#L51).  
+If you'd like to poll for the answer, use [solve](https://github.com/qwbarch/2captcha-haskell/blob/e442950a79f1aef7fd2fb95aac9cd9bfe41a8df7/src/TwoCaptcha/Internal/Client.hs#L54).
+
+[Default timeout](https://github.com/qwbarch/2captcha-haskell/blob/e442950a79f1aef7fd2fb95aac9cd9bfe41a8df7/src/TwoCaptcha/Internal/Types/Captcha.hs#L22): 120 seconds.  
+[ReCAPTCHA timeout](https://github.com/qwbarch/2captcha-haskell/blob/e442950a79f1aef7fd2fb95aac9cd9bfe41a8df7/src/TwoCaptcha/Internal/Types/ReCaptcha.hs#L118): 600 seconds.  
+[Polling interval](https://github.com/qwbarch/2captcha-haskell/blob/e442950a79f1aef7fd2fb95aac9cd9bfe41a8df7/src/TwoCaptcha/Internal/Types/Captcha.hs#L26): 10 seconds
+
+Below are the minimal required parameters to solve each type of captcha.
+
+### Normal Captcha
+
+This function can be used to solve a local image captcha.
+
+```haskell
+let captcha =
+      normalCaptcha
+        & apiKey ?~ "YOUR_API_KEY"
+        & method ?~ "post"
+        & file ?~ "/path/to/file"
+session <- newAPISession
+solve session captcha pollingInterval captchaTimeout
+```
+
+This function can be used to solve a captcha encoded in base-64 form.
+
+```haskell
+let captcha =
+  normalCaptcha
+    & apiKey ?~ "YOUR_API_KEY"
+    & method ?~ "base64"
+    & body ?~ "BASE_64_ENCODED_IMAGE"
+session <- newAPISession
+solve session captcha pollingInterval captchaTimeout
+```
+
+### Text Captcha
+
+This function can be used to solve captchas in text form.
+
+```haskell
+let captcha =
+      textCaptcha
+        & apiKey ?~ "YOUR_API_KEY"
+        & textContent ?~ "If tomorrow is Saturday, what day is it today?"
+session <- newAPISession
+solve session captcha pollingInterval captchaTimeout
+```
+
+### ReCAPTCHA v2
+
+ReCAPTCHA v2 requires a ``k`` or ``data-sitekey`` value found on the captcha page.
+
+```haskell
+let captcha =
+      reCAPTCHAV2
+        & apiKey ?~ "YOUR_API_KEY"
+        & googleKey ?~ "GOOGLE_KEY_VALUE"
+        & pageUrl ?~ "PAGE_URL"
+session <- newAPISession
+solve session captcha pollingInterval reCAPTCHATimeout
+```
+
+### ReCaptcha v3
+
+ReCAPTCHA v3 requires a ``k`` or ``data-sitekey`` value found on the captcha page.
+
+```haskell
+let captcha =
+      reCAPTCHAV3
+        & apiKey ?~ "YOUR_API_KEY"
+        & googleKey ?~ "GOOGLE_KEY_VALUE"
+        & pageUrl ?~ "PAGE_URL"
+session <- newAPISession
+solve session captcha pollingInterval reCAPTCHATimeout
+```
+
+### FunCaptcha
+
+FunCaptcha (ArkoseLabs) requires a ``pk`` or ``data-pkey`` value found on the captcha page.
+
+```haskell
+let captcha =
+      funCaptcha
+        & apiKey ?~ "YOUR_API_KEY"
+        & publicKey ?~ "PUBLIC_KEY"
+        & pageUrl ?~ "PAGE_URL"
+session <- newAPISession
+solve session captcha pollingInterval captchaTimeout
+```
+
+### GeeTest
+
+GeeTest requires the ``gt``, ``challenge``, and ``api_server`` values on the captcha page.
+
+```haskell
+let captcha =
+      geeTestCaptcha
+        & apiKey ?~ "YOUR_API_KEY"
+        & gt ?~ "GT_VALUE"
+        & apiServer ?~ "API_SERVER"
+        & challenge ?~ "CAPTCHA_CHALLENGE"
+        & pageUrl ?~ "PAGE_URL"
+session <- newAPISession
+solve session captcha pollingInterval captchaTimeout
+```
+
+### hCaptcha
+
+hCaptcha requires the ``data-sitekey`` value on the captcha page.
+
+```haskell
+let captcha =
+      hCaptcha
+        & apiKey ?~ "YOUR_API_KEY"
+        & siteKey ?~ "SITE_KEY"
+        & pageUrl ?~ "PAGE_URL"
+session <- newAPISession
+solve session captcha pollingInterval captchaTimeout
+```
+
+### KeyCaptcha
+
+KeyCaptcha requires the ``s_s_c_user_id``, ``s_s_c_session_id``, ``s_s_c_web_server_sign``, and ``s_s_c_web_server_sign2`` values on the captcha page.
+
+```haskell
+let captcha =
+      keyCaptcha
+        & apiKey ?~ "YOUR_API_KEY"
+        & userId ?~ "USER_ID_VALUE"
+        & sessionId ?~ "SESSION_ID_VALUE"
+        & webServerSign ?~ "WEB_SERVER_SIGN_VALUE"
+        & webServerSign2 ?~ "WEB_SERVER_SIGN_2_VALUE"
+        & pageUrl ?~ "PAGE_URL"
+session <- newAPISession
+solve session captcha pollingInterval captchaTimeout
+```
+
+### Capy
+
+Capy requires the ``captchakey`` value on the captcha page.
+
+```haskell
+let captcha =
+      capyCaptcha
+        & apiKey ?~ "YOUR_API_KEY"
+        & captchaKey ?~ "CAPTCHA_KEY"
+        & pageUrl ?~ "PAGE_URL"
+        & scriptDomain ?~ "SCRIPT_DOMAIN"
+session <- newAPISession
+solve session captcha pollingInterval captchaTimeout
+```
+
+### Grid
+
+This function can be used to solve a local grid image captcha.
+
+```haskell
+let captcha =
+      gridCaptcha
+        & apiKey ?~ "YOUR_API_KEY"
+        & method ?~ "post"
+        & file ?~ "/path/to/file"
+session <- newAPISession
+solve session captcha pollingInterval captchaTimeout
+```
+
+### Coordinate
+
+This function can be used to solve a local coordinate image captcha.
+
+```haskell
+let captcha =
+      coordinateCaptcha
+        & apiKey ?~ "YOUR_API_KEY"
+        & method ?~ "post"
+        & file ?~ "/path/to/file"
+session <- newAPISession
+solve session captcha pollingInterval captchaTimeout
+```
+
+### Rotate
+
+This function can be used to solve a local rotate image captcha.
+
+```haskell
+let captcha =
+      rotateCaptcha
+        & apiKey ?~ "YOUR_API_KEY"
+        & file ?~ "/path/to/file"
+session <- newAPISession
+solve session captcha pollingInterval captchaTimeout
+```
+
+### TikTok
+
+TikTok requires the ``aid`` and ``host`` values on the captcha page.
+
+```haskell
+let captcha =
+      tikTokCaptcha
+        & apiKey ?~ "YOUR_API_KEY"
+        & cookies ?~ "YOUR_COOKIES"
+        & aid ?~ 0
+        & host ?~ "HOST_VALUE"
+        & pageUrl ?~ "PAGE_URL"
+session <- newAPISession
+solve session captcha pollingInterval captchaTimeout
+```
+
+
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/src/TwoCaptcha/Captcha.hs b/src/TwoCaptcha/Captcha.hs
new file mode 100644
--- /dev/null
+++ b/src/TwoCaptcha/Captcha.hs
@@ -0,0 +1,41 @@
+module TwoCaptcha.Captcha
+  ( module TwoCaptcha.Internal.Types.Captcha,
+    module TwoCaptcha.Internal.Types.CapyCaptcha,
+    module TwoCaptcha.Internal.Types.CoordinateCaptcha,
+    module TwoCaptcha.Internal.Types.Exception,
+    module TwoCaptcha.Internal.Types.FunCaptcha,
+    module TwoCaptcha.Internal.Types.GeeTestCaptcha,
+    module TwoCaptcha.Internal.Types.GridCaptcha,
+    module TwoCaptcha.Internal.Types.HCaptcha,
+    module TwoCaptcha.Internal.Types.KeyCaptcha,
+    module TwoCaptcha.Internal.Types.NormalCaptcha,
+    module TwoCaptcha.Internal.Types.ReCaptcha,
+    module TwoCaptcha.Internal.Types.RotateCaptcha,
+    module TwoCaptcha.Internal.Types.TextCaptcha,
+    module TwoCaptcha.Internal.Types.TikTokCaptcha,
+    module TwoCaptcha.Internal.Client,
+  )
+where
+
+import TwoCaptcha.Internal.Client hiding (handle)
+import TwoCaptcha.Internal.Types.Captcha hiding
+  ( defaultCaptcha,
+    mkParamLens,
+    mkParamLens',
+    mkParamLensBool,
+    mkPartLens,
+    mkPartTextLens,
+  )
+import TwoCaptcha.Internal.Types.CapyCaptcha
+import TwoCaptcha.Internal.Types.CoordinateCaptcha
+import TwoCaptcha.Internal.Types.Exception
+import TwoCaptcha.Internal.Types.FunCaptcha
+import TwoCaptcha.Internal.Types.GeeTestCaptcha
+import TwoCaptcha.Internal.Types.GridCaptcha
+import TwoCaptcha.Internal.Types.HCaptcha
+import TwoCaptcha.Internal.Types.KeyCaptcha
+import TwoCaptcha.Internal.Types.NormalCaptcha
+import TwoCaptcha.Internal.Types.ReCaptcha
+import TwoCaptcha.Internal.Types.RotateCaptcha
+import TwoCaptcha.Internal.Types.TextCaptcha
+import TwoCaptcha.Internal.Types.TikTokCaptcha
diff --git a/src/TwoCaptcha/Internal/Client.hs b/src/TwoCaptcha/Internal/Client.hs
new file mode 100644
--- /dev/null
+++ b/src/TwoCaptcha/Internal/Client.hs
@@ -0,0 +1,84 @@
+module TwoCaptcha.Internal.Client where
+
+import Control.Concurrent (threadDelay)
+import Control.Lens ((&), (.~), (?~), (^.), (^?))
+import Control.Monad.Catch (MonadCatch, MonadThrow (throwM), try)
+import Control.Monad.IO.Class (MonadIO (liftIO))
+import Data.Aeson.Lens (key, _Integer, _String)
+import Data.ByteString.Lazy.Char8 (ByteString)
+import Data.Text (Text, unpack)
+import GHC.Base (Coercible, coerce)
+import Network.Wreq (Response, responseBody)
+import Network.Wreq.Session (Session, getWith, postWith)
+import System.Clock (Clock (Monotonic), getTime, toNanoSecs)
+import TwoCaptcha.Internal.Types.Captcha (Captcha, CaptchaId, CaptchaRes (CaptchaRes), HasCaptchaLenses, HasCommonCaptchaLenses (apiKey, headerACAO), PollingInterval, TimeoutDuration, captchaId, captchaRes, options, parts)
+import TwoCaptcha.Internal.Types.Exception (TwoCaptchaErrorCode (CaptchaNotReady), TwoCaptchaException (NetworkException, SolvingTimeout, TwoCaptchaResponseException, UnknownError), readErrorCode)
+
+-- | Runs the given http method and adapts errors to 'TwoCaptchaException'.
+handle :: (MonadIO m, MonadCatch m) => IO (Response ByteString) -> m Text
+handle method =
+  try (liftIO method) >>= \case
+    -- HttpException found due to non-200 status code. Rethrow as NetworkException.
+    Left exception -> throwM $ NetworkException exception
+    Right response -> do
+      let statusRequest = do
+            body <- response ^? responseBody
+            status <- body ^? key "status" . _Integer
+            request <- body ^? key "request" . _String
+            return (status, request)
+      case statusRequest of
+        -- 'status' and 'request' fields are missing.
+        Nothing -> do
+          liftIO $ print response
+          throwM $ UnknownError "The response is not the expected JSON. This is likely due to 2captcha changing their API."
+        Just (status, request) -> do
+          -- 'status' 0 means an error was returned.
+          if status == 0
+            then case readErrorCode (unpack request) of
+              -- Parsing the error code failed even though an error was returned. This means the error code is not known yet.
+              Left _ -> throwM $ UnknownError ("Invalid error: " <> request)
+              -- Parsing the error code succeeded.
+              Right errorCode -> throwM $ TwoCaptchaResponseException errorCode
+            else -- No error was found.
+              pure request
+
+-- | Encapsulates the __in.php__ and __res.php__ endpoints for the 2captcha API.
+class TwoCaptchaClient m where
+  -- | Submit a captcha to be solved by the 2captcha API. Returns a captcha id used for 'answer'.
+  submit :: (Coercible Captcha a, HasCaptchaLenses a, HasCommonCaptchaLenses a) => Session -> a -> m CaptchaId
+
+  -- | Attempt to retrieve the answer of a captcha previously submitted.
+  answer :: Session -> CaptchaRes -> m Text
+
+  -- | Submits a captcha and polls for the answer.
+  solve :: (Coercible Captcha a, HasCaptchaLenses a, HasCommonCaptchaLenses a) => PollingInterval -> TimeoutDuration -> Session -> a -> m Text
+
+instance (MonadIO m, MonadCatch m) => TwoCaptchaClient m where
+  submit session captcha' = handle $ postWith (captcha ^. options) session "https://2captcha.com/in.php" (captcha ^. parts)
+    where
+      captcha = coerce captcha'
+  answer session (CaptchaRes captchaRes) = handle $ getWith (captchaRes ^. options) session "https://2captcha.com/res.php"
+  solve pollingInterval timeoutDuration session captcha = do
+    captchaId' <- submit session captcha
+    let captchaRes' =
+          captchaRes
+            & apiKey .~ (captcha ^. apiKey)
+            & headerACAO .~ (captcha ^. headerACAO)
+            & captchaId ?~ captchaId'
+    let time = liftIO $ (\t -> toNanoSecs t `div` 1000000) <$> getTime Monotonic
+    startTime <- time
+    let pollAnswer previousTime currentTime =
+          -- Elapsed time is past the timeout duration
+          if currentTime - previousTime >= timeoutDuration
+            then throwM SolvingTimeout
+            else do
+              liftIO $ threadDelay (pollingInterval * 1000)
+              -- Attempt to retrieve the answer. If it's not ready yet, retry.
+              answerAttempt <- try $ answer session captchaRes'
+              case answerAttempt of
+                Left (TwoCaptchaResponseException CaptchaNotReady) -> do
+                  updatedTime <- time
+                  pollAnswer currentTime updatedTime
+                Left exception -> throwM exception
+                Right answer -> pure answer
+    pollAnswer startTime startTime
diff --git a/src/TwoCaptcha/Internal/Types/Captcha.hs b/src/TwoCaptcha/Internal/Types/Captcha.hs
new file mode 100644
--- /dev/null
+++ b/src/TwoCaptcha/Internal/Types/Captcha.hs
@@ -0,0 +1,208 @@
+module TwoCaptcha.Internal.Types.Captcha where
+
+import Control.Lens (Getter, Lens', iso, lens, to, (%~), (&), (.~), (^.))
+import Control.Lens.TH (makeLenses)
+import Data.String (IsString)
+import Data.Text (Text, pack, unpack)
+import GHC.Base (Coercible, coerce)
+import Network.Wreq (Options, Part, defaults, param, partFile, partText)
+
+-- import Prelude hiding (lookup)
+
+-- | The id of a captcha being solved.
+type CaptchaId = Text
+
+-- | Time in milliseconds in how often to request the answer.
+type PollingInterval = Int
+
+-- | Time in milliseconds on when to timeout if the request takes too long.
+type TimeoutDuration = Integer
+
+-- | Default captcha timeout duration (120 seconds).
+captchaTimeout :: TimeoutDuration
+captchaTimeout = 120000
+
+-- | Default polling interval (10 seconds).
+pollingInterval :: PollingInterval
+pollingInterval = 10000
+
+-- | Represents the request information required to solve a captcha.
+data Captcha = MkCaptcha
+  { _options :: Options,
+    _partTexts :: [(Text, Text)],
+    _partFiles :: [(Text, FilePath)]
+  }
+  deriving (Show)
+
+makeLenses ''Captcha
+
+-- | Convert the captcha's multipart form parameters into a ['Part'].
+parts :: Getter Captcha [Part]
+parts = to (\captcha -> (uncurry partText <$> captcha ^. partTexts) ++ (uncurry partFile <$> captcha ^. partFiles))
+
+instance HasCommonCaptchaLenses Captcha
+
+instance HasCaptchaLenses Captcha
+
+-- | Default parameters for solving a captcha. Internal use only.
+defaultCaptcha :: Captcha
+defaultCaptcha = MkCaptcha (defaults & param "json" .~ ["1"]) [] []
+
+-- | Create a lens using the given field name for multipart forms.
+mkPartLens :: (Coercible Captcha a, IsString s) => Lens' Captcha [(Text, s)] -> Text -> Lens' a (Maybe s)
+mkPartLens partLens field = lens getter setter
+  where
+    getter a = lookup field (coerce a ^. partLens)
+    setter a (Just value) = coerce $ coerce a & partLens %~ ((field, value) :)
+    setter a Nothing = a
+
+-- | Create a lens using the given field name for multipart form texts.
+mkPartTextLens :: Coercible Captcha a => Text -> Lens' a (Maybe Text)
+mkPartTextLens = mkPartLens partTexts
+
+-- | Create a lens using the given field name for multipart form files.
+mkPartFileLens :: Coercible Captcha a => Text -> Lens' a (Maybe FilePath)
+mkPartFileLens = mkPartLens partFiles
+
+-- | Creates a lens using the given field name for query parameters.
+mkParamLens :: Coercible Captcha a => Text -> Lens' a (Maybe Text)
+mkParamLens field = lens getter setter
+  where
+    getter a =
+      let value = coerce a ^. options . param field
+       in if null value then Nothing else Just $ head value
+    setter a (Just value) = coerce $ coerce a & options %~ (& param field .~ [value])
+    setter a Nothing = a
+
+-- |
+-- Create a lens using the given field name for type __b__ with a 'Show' and 'Read' instance.
+--
+-- GOTCHA: Bool values translate to 'True' or 'False'. Use 'mkLensBool' instead for bool lenses.
+mkParamLens' :: (Coercible Captcha a, Show b, Read b) => Text -> Lens' a (Maybe b)
+mkParamLens' field = mkParamLens field . iso (read . unpack <$>) (pack . show <$>)
+
+-- |
+-- Create a lens using the given field name for bools.
+--
+-- The boolean values become:
+--
+-- * 'True' - 1
+-- * 'False' - 0
+mkParamLensBool :: Coercible Captcha a => Text -> Lens' a (Maybe Bool)
+mkParamLensBool field = mkParamLens field . iso (textToBool <$>) (boolToText <$>)
+  where
+    textToBool "0" = False
+    textToBool "1" = True
+    textToBool number = error $ unpack number <> " is not a valid bool value. This should never occur, please don't manipulate the options manually."
+    boolToText False = "0"
+    boolToText True = "1"
+
+-- | Lenses for constructing options for 'TwoCaptcha.Internal.Client.submit'.
+class Coercible Captcha a => HasCaptchaLenses a where
+  -- | Software developer id. Developers who integrate their software with 2captcha earn 10% of the user's spendings.
+  softId :: Lens' a (Maybe Int)
+  softId = mkParamLens' "soft_id"
+
+  -- | URL for <https://2captcha.com/2captcha-api#pingback pingback> (callback) response that will be sent the answer to when the captcha is solved.
+  pingback :: Lens' a (Maybe Text)
+  pingback = mkParamLens "pingback"
+
+  -- | Type of captcha to solve.
+  method :: Lens' a (Maybe Text)
+  method = mkParamLens "method"
+
+-- | Lenses for constructing options for 'TwoCaptcha.Internal.Client.submit' and 'TwoCaptcha.Internal.Client.answer'.
+class Coercible Captcha a => HasCommonCaptchaLenses a where
+  -- | Your 2captcha API <https://2captcha.com/2captcha-api#solving_captchas key>.
+  apiKey :: Lens' a (Maybe Text)
+  apiKey = mkParamLens "key"
+
+  -- |
+  -- If True, 'TwoCaptcha.Internal.Client.submit' will include the __Access-Control-Allow-Origin:*__ header in the response.
+  -- Used for cross-domain AJAX requests in web applications.
+  headerACAO :: Lens' a (Maybe Bool)
+  headerACAO = mkParamLensBool "header_acao"
+
+-- | Parameters used to retrieve the 'TwpCaptcha.Internal.Client.answer' of a solved captcha.
+newtype CaptchaRes = CaptchaRes Captcha deriving (Show)
+
+instance HasCommonCaptchaLenses CaptchaRes
+
+-- |
+-- Parameters for retrieving a captcha's answer.
+--
+-- Required parameters:
+--
+-- * 'apiKey'
+-- * 'captchaId'
+--
+-- Optional parameters:
+--
+-- * 'headerACAO'
+captchaRes :: CaptchaRes
+captchaRes = CaptchaRes (defaultCaptcha & options %~ (& param "action" .~ ["get"]))
+
+-- | The captcha id returned from 'TwoCaptcha.Internal.Client.submit'.
+captchaId :: Lens' CaptchaRes (Maybe Text)
+captchaId = mkParamLens "id"
+
+class Coercible Captcha a => HasPageURL a where
+  -- | Full URL of the page where the captcha is found.
+  pageUrl :: Lens' a (Maybe Text)
+  pageUrl = mkParamLens "pageurl"
+
+class Coercible Captcha a => HasProxy a where
+  -- |
+  -- Proxy to be sent to the worker who solves the captcha. You can read more about proxies <https://2captcha.com/2captcha-api#proxies here>.
+  --
+  -- Format must be in __login:password@123.123.123.123:3128__ .
+  proxy :: Lens' a (Maybe Text)
+  proxy = mkParamLens "proxy"
+
+  -- | Type of your proxy: __HTTP__, __HTTPS__, __SOCKS4__, __SOCKS5__.
+  proxyType :: Lens' a (Maybe Text)
+  proxyType = mkParamLens "proxytype"
+
+class Coercible Captcha a => HasLocalImage a where
+  -- | File path of a captcha image.
+  file :: Lens' a (Maybe FilePath)
+  file = mkPartFileLens "file"
+
+  -- | Base-64 encoded image.
+  body :: Lens' a (Maybe Text)
+  body = mkPartTextLens "body"
+
+  -- | Text which is shown to the worker to help solve a captcha.
+  textInstructions :: Lens' a (Maybe Text)
+  textInstructions = mkParamLens "textInstructions"
+
+  -- | Image file path with instructions on solving a captcha.
+  imgInstructions :: Lens' a (Maybe FilePath)
+  imgInstructions = mkPartFileLens "imgInstructions"
+
+class Coercible Captcha a => HasLanguage a where
+  -- |
+  -- The captcha's language:
+  --
+  -- 0. Not specified.
+  -- 1. Cyrillic captcha.
+  -- 2. Latin captcha.
+  language :: Lens' a (Maybe Int)
+  language = mkParamLens' "language"
+
+  -- | The captcha's language code. Click <https://2captcha.com/2captcha-api#language here> for a list of supported languages.
+  languageCode :: Lens' a (Maybe Text)
+  languageCode = mkParamLens "lang"
+
+class Coercible Captcha a => HasUserAgent a where
+  -- | User agent that will be used by the worker when solving the captcha.
+  userAgent :: Lens' a (Maybe Text)
+  userAgent = mkParamLens "userAgent"
+
+class Coercible Captcha a => HasCookies a where
+  -- |
+  -- Cookies that will be used by the worker solving the captcha. The used cookies will also be included in the response.
+  --
+  -- Format: __KEY1:Value1;KEY2:Value2;__
+  cookies :: Lens' a (Maybe Text)
+  cookies = mkParamLens "cookies"
diff --git a/src/TwoCaptcha/Internal/Types/CapyCaptcha.hs b/src/TwoCaptcha/Internal/Types/CapyCaptcha.hs
new file mode 100644
--- /dev/null
+++ b/src/TwoCaptcha/Internal/Types/CapyCaptcha.hs
@@ -0,0 +1,48 @@
+module TwoCaptcha.Internal.Types.CapyCaptcha where
+
+import Control.Lens (Lens', (&), (?~))
+import Data.Text (Text)
+import TwoCaptcha.Internal.Types.Captcha (Captcha, HasCaptchaLenses (method), HasCommonCaptchaLenses, HasPageURL, HasProxy, defaultCaptcha, mkParamLens)
+
+-- | Parameters used to solve a capy puzzle captcha.
+newtype CapyCaptcha = MkCapyCaptcha Captcha deriving (Show)
+
+instance HasCommonCaptchaLenses CapyCaptcha
+
+instance HasCaptchaLenses CapyCaptcha
+
+instance HasPageURL CapyCaptcha
+
+instance HasProxy CapyCaptcha
+
+-- |
+-- Parameters used to solve a capy puzzle captcha.
+--
+-- Required parameters:
+--
+-- * 'TwoCaptcha.Internal.Types.Captcha.apiKey'
+-- * 'captchaKey'
+-- * 'scriptDomain'
+-- * 'TwoCaptcha.Internal.Types.Captcha.pageUrl'
+--
+-- Optional parameters:
+--
+-- * 'scriptDomain'
+-- * 'TwoCaptcha.Internal.Types.Captcha.headerACAO'
+-- * 'TwoCaptcha.Internal.Types.Captcha.pingback'
+-- * 'TwoCaptcha.Internal.Types.Captcha.softId'
+-- * 'TwoCaptcha.Internal.Types.Captcha.proxy'
+-- * 'TwoCaptcha.Internal.Types.Captcha.proxyType'
+capyCaptcha :: CapyCaptcha
+capyCaptcha = MkCapyCaptcha (defaultCaptcha & method ?~ "capy")
+
+-- | Value of __captchakey__ parameter you found on page.
+captchaKey :: Lens' CapyCaptcha (Maybe Text)
+captchaKey = mkParamLens "captchakey"
+
+-- |
+-- The domain part of the script URL found on page.
+--
+-- If not specified, defaults to: https://jp.api.capy.me/
+scriptDomain :: Lens' CapyCaptcha (Maybe Text)
+scriptDomain = mkParamLens "api_server"
diff --git a/src/TwoCaptcha/Internal/Types/CoordinateCaptcha.hs b/src/TwoCaptcha/Internal/Types/CoordinateCaptcha.hs
new file mode 100644
--- /dev/null
+++ b/src/TwoCaptcha/Internal/Types/CoordinateCaptcha.hs
@@ -0,0 +1,51 @@
+module TwoCaptcha.Internal.Types.CoordinateCaptcha where
+
+import Control.Lens (Lens')
+import TwoCaptcha.Internal.Types.Captcha (Captcha, HasCaptchaLenses, HasCommonCaptchaLenses, HasLanguage, HasLocalImage, defaultCaptcha, mkParamLensBool)
+
+-- | Parameters used to solve a coordinate captcha.
+newtype CoordinateCaptcha = MkCoordinateCaptcha Captcha deriving (Show)
+
+instance HasCommonCaptchaLenses CoordinateCaptcha
+
+instance HasCaptchaLenses CoordinateCaptcha
+
+instance HasLocalImage CoordinateCaptcha
+
+instance HasLanguage CoordinateCaptcha
+
+-- | Parameters used to solve a coordinate captcha.
+--
+-- Required parameters:
+--
+-- * 'TwoCaptcha.Internal.Types.Captcha.apiKey'
+-- * 'TwoCaptcha.Internal.Types.Captcha.method'
+-- * 'coordinateReCAPTCHAImage'
+-- * 'TwoCaptcha.Internal.Types.Captcha.file'*
+-- * 'TwoCaptcha.Internal.Types.Captcha.body'*
+--
+-- Optional parameters:
+--
+-- * 'TwoCaptcha.Internal.Types.Captcha.textInstructions'
+-- * 'TwoCaptcha.Internal.Types.Captcha.imgInstructions'
+-- * 'TwoCaptcha.Internal.Types.Captcha.language'
+-- * 'TwoCaptcha.Internal.Types.Captcha.languageCode'
+-- * 'TwoCaptcha.Internal.Types.Captcha.headerACAO'
+-- * 'TwoCaptcha.Internal.Types.Captcha.pingback'
+-- * 'TwoCaptcha.Internal.Types.Captcha.softId'
+--
+-- Possible 'TwoCaptcha.Internal.Types.Captcha.method' values:
+--
+-- * __post__ - defines that you're sending an image with multipart form
+-- * __base64__  - defines that you're sending a base64 encoded image
+--
+-- Starred required parameter rules:
+--
+-- * __file__ is only required if __captcha = "post"__
+-- * __body__ is only required if __captcha = "base64"__
+coordinateCaptcha :: CoordinateCaptcha
+coordinateCaptcha = MkCoordinateCaptcha defaultCaptcha
+
+-- | Defines that you're sending a reCAPTCHA as an image.
+coordinateReCAPTCHAImage :: Lens' CoordinateCaptcha (Maybe Bool)
+coordinateReCAPTCHAImage = mkParamLensBool "coordinatescaptcha"
diff --git a/src/TwoCaptcha/Internal/Types/Exception.hs b/src/TwoCaptcha/Internal/Types/Exception.hs
new file mode 100644
--- /dev/null
+++ b/src/TwoCaptcha/Internal/Types/Exception.hs
@@ -0,0 +1,242 @@
+module TwoCaptcha.Internal.Types.Exception where
+
+import Control.Exception (Exception)
+import Data.Functor (($>))
+import Data.Text (Text)
+import Network.HTTP.Client (HttpException)
+import Text.Parsec (ParseError, parse, try, (<|>))
+import Text.Parsec.Char (string)
+import Text.Parsec.String (Parser)
+
+-- | Represents a possible exception when interacting with the 2captcha API.
+data TwoCaptchaException
+  = -- | An error documented on 2captcha's website.
+    TwoCaptchaResponseException TwoCaptchaErrorCode
+  | -- | A non-200 status code was thrown. This should only appear in rare cases.
+    NetworkException HttpException
+  | -- | An unknown error occured, likely due to a change in the 2captcha API.
+    UnknownError Text
+  | -- | Solving the captcha took too long. Try setting a higher timeout duration?
+    SolvingTimeout
+  deriving (Show)
+
+instance Exception TwoCaptchaException
+
+-- | Possible errors when using the 2captcha API.
+data TwoCaptchaErrorCode
+  = -- | The api key you provided is invalid. Please ensure it is 32 characters long.
+    WrongUserKey
+  | -- | The key you've provided does not exist.
+    KeyDoesNotExist
+  | -- | You don't have funds in your account.
+    ZeroBalance
+  | -- | The __pageurl__ parameter is missing in your request.
+    PageUrlMissing
+  | -- |
+    -- You can receive this error in two cases:
+    --
+    -- 1. __If you solve token-based captchas (reCAPTCHA, hCaptcha, ArkoseLabs FunCaptcha, GeeTest, etc):__
+    -- the queue of your captchas that are not distributed to workers is too long.
+    -- Queue limit changes dynamically and depends on total amount of captchas awaiting solution and usually it’s between 50 and 100 captchas.
+    --
+    -- 2. __If you solve Normal Captcha:__ your maximum rate for normal captchas is lower than current rate on the server.
+    -- You can change your maximum rate in <https://2captcha.com/setting your account's settings.>
+    NoSlotAvailable
+  | -- | Image size is less than 100 bytes.
+    ZeroCaptchaFileSize
+  | -- | Image size is more than 100 kB.
+    TooBigCaptchaFileSize
+  | -- | Image file has unsupported extension. Accepted extensions: jpg, jpeg, gif, png.
+    WrongFileExtension
+  | -- | Server can't recognize image file type.
+    ImageTypeNotSupported
+  | -- |
+    -- Server can't get file data from your POST-request.
+    -- That happens if your POST-request is malformed or base64 data is not a valid base64 image.
+    UploadFailure
+  | -- | The request is sent from the IP that is not on the list of your allowed IPs.
+    IpNotAllowed
+  | -- | Your IP address is banned due to many frequent attempts to access the server using wrong authorization keys.
+    IpBanned
+  | -- |
+    -- You can get this error code when sending reCAPTCHA V2. This happens if your request contains invalid pair of googlekey and pageurl.
+    -- The common reason for that is that reCAPTCHA is loaded inside an iframe hosted on another domain/subdomain.
+    BadTokenOrPageUrl
+  | -- | You can get this error code when sending reCAPTCHA V2. That means that sitekey value provided in your request is incorrect: it's blank or malformed.
+    GoogleKeyInvalid
+  | -- | The __googlekey__ parameter is missing in your request.
+    GoogleKeyMissing
+  | -- |
+    -- You've sent an image that is marked in 2captcha's database as unrecognizable.
+    -- Usually that happens if the website where you found the captcha stopped sending you captchas and started to send a "deny access" image.
+    CaptchaImageBlocked
+  | -- | You are sending too many unrecognizable images.
+    TooManyBadImages
+  | -- |
+    -- You made more than 60 requests to in.php within 3 seconds.
+    -- Your account is banned for 10 seconds. Ban will be lifted automatically.
+    RateLimited
+  | -- |
+    -- You received the error 'NoSlotAvailable' 120 times in one minute because your current bid is lower than current bid on the server.
+    --
+    -- Blocking time: 10 minutes.
+    Error1001
+  | -- |
+    -- You received the error 'ZeroBalance' 120 times in one minute because your balance is zero.
+    --
+    -- Blocking time: 5 minutes.
+    Error1002
+  | -- |
+    -- You received the error 'NoSlotAvailable' because you are uploading many captchas and server has a long queue of your captchas that are not distributed to workers.
+    -- You received three times more errors than amount of captchas you sent (but not less than 120 errors). Increase the timeout if you see this error.
+    --
+    -- Blocking time: 30 seconds.
+    Error1003
+  | -- | Your IP address is blocked because there were 5 requests with incorrect API key from your IP.
+    Error1004
+  | -- |
+    -- You are making too many requests to res.php to get answers.
+    --
+    --
+    -- 2captcha uses the following rule to block your account: R > C * 20 + 1200
+    --
+    -- Where:
+    --
+    -- * R - the amount of your requests
+    --
+    -- * C - the amount of captchas you've uploaded
+    --
+    -- That means that you don't have to make more than 20 requests to res.php per each captcha.
+    -- Please remember that balance request sent to res.php also counts!
+    --
+    -- To get your answer faster without a risk to be blocked you can use <https://2captcha.com/2captcha-api#pingback pingback> feature and 2captcha will send you the answer when your captcha is solved.
+    --
+    -- Blocking time: 10 minutes.
+    Error1005
+  | -- |
+    -- The error code is returned if some required parameters are missing in your request or the values have incorrect format.
+    -- For example if you submit <https://2captcha.com/2captcha-api#grid Grid images> but your request is missing an instruction for workers.
+    --
+    -- Blocking time: 5 minutes.
+    BadParameters
+  | -- | You can get this error code when sending a captcha via proxy server which is marked as BAD by the 2captcha API.
+    BadProxy
+  | -- | Your captcha is not solved yet.
+    CaptchaNotReady
+  | -- |
+    -- 2captcha was unable to solve your captcha - three of their workers were unable solve it or they didn't get an answer within 90 seconds (300 seconds for reCAPTCHA V2).
+    --
+    -- You will not be charged for that request.
+    CaptchaUnsolvable
+  | -- | You've provided captcha ID in wrong format. The ID can contain numbers only.
+    WrongIdFormat
+  | -- | You provided an invalid captcha id.
+    WrongCaptchaId
+  | -- | Error is returned when 100% accuracy feature is enabled. The error means that max numbers of tries is reached but min number of matches not found.
+    BadDuplicates
+  | -- |
+    -- Error is returned to your <https://2captcha.com/2captcha-api#complain report> request if you already complained lots of correctly solved captchas (more than 40%).
+    -- Or if more than 15 minutes passed after you submitted the captcha.
+    ReportNotRecorded
+  | -- | Error is returned to your <https://2captcha.com/2captcha-api#complain report request> if you are trying to report the same captcha more than once.
+    DuplicateReport
+  | -- |
+    -- You can receive this error code when registering a <https://2captcha.com/2captcha-api#pingback pingback (callback)> IP or domain.
+    --
+    -- This happens if your request is coming from an IP address that doesn't match the IP address of your pingback IP or domain.
+    InvalidPingbackIp
+  | -- |
+    -- You can receive this error code when sending <https://2captcha.com/2captcha-api#solving_geetest GeeTest>.
+    -- This error means the __challenge__ value you provided is expired.
+    TokenExpired
+  | -- | Action parameter is missing or no value is provided for __action__ parameter.
+    EmptyAction
+  | -- |
+    -- You can get this error code if we were unable to load a captcha through your proxy server.
+    -- The proxy will be marked as BAD by our API and we will not accept requests with the proxy during 10 minutes.
+    -- You will recieve ERROR_BAD_PROXY code from in.php API endpoint in such case.
+    ProxyConnectionFailed
+  deriving (Show, Eq)
+
+-- | The raw error code provided by 2captcha.
+errorCode :: TwoCaptchaErrorCode -> String
+errorCode WrongUserKey = "ERROR_WRONG_USER_KEY"
+errorCode KeyDoesNotExist = "ERROR_KEY_DOES_NOT_EXIST"
+errorCode ZeroBalance = "ERROR_ZERO_BALANCE"
+errorCode PageUrlMissing = "ERROR_PAGEURL"
+errorCode NoSlotAvailable = "ERROR_NO_SLOT_AVAILABLE"
+errorCode ZeroCaptchaFileSize = "ERROR_ZERO_CAPTCHA_FILESIZE"
+errorCode TooBigCaptchaFileSize = "ERROR_TOO_BIG_CAPTCHA_FILESIZE"
+errorCode WrongFileExtension = "ERROR_WRONG_FILE_EXTENSION"
+errorCode ImageTypeNotSupported = "ERROR_IMAGE_TYPE_NOT_SUPPORTED"
+errorCode UploadFailure = "ERROR_UPLOAD"
+errorCode IpNotAllowed = "ERROR_IP_NOT_ALLOWED"
+errorCode IpBanned = "IP_BANNED"
+errorCode BadTokenOrPageUrl = "ERROR_BAD_TOKEN_OR_PAGEURL"
+errorCode GoogleKeyInvalid = "ERROR_GOOGLEKEY"
+errorCode GoogleKeyMissing = "ERROR_WRONG_GOOGLEKEY"
+errorCode CaptchaImageBlocked = "ERROR_CAPTCHAIMAGE_BLOCKED"
+errorCode TooManyBadImages = "TOO_MANY_BAD_IMAGES"
+errorCode RateLimited = "MAX_USER_TURN"
+errorCode Error1001 = "ERROR: 1001"
+errorCode Error1002 = "ERROR: 1002"
+errorCode Error1003 = "ERROR: 1003"
+errorCode Error1004 = "ERROR: 1004"
+errorCode Error1005 = "ERROR: 1005"
+errorCode BadParameters = "ERROR_BAD_PARAMETERS"
+errorCode BadProxy = "ERROR_BAD_PROXY"
+errorCode CaptchaNotReady = "CAPCHA_NOT_READY"
+errorCode CaptchaUnsolvable = "ERROR_CAPTCHA_UNSOLVABLE"
+errorCode WrongIdFormat = "ERROR_WRONG_ID_FORMAT"
+errorCode WrongCaptchaId = "ERROR_WRONG_CAPTCHA_ID"
+errorCode BadDuplicates = "ERROR_BAD_DUPLICATES"
+errorCode ReportNotRecorded = "ERROR_REPORT_NOT_RECORDED"
+errorCode DuplicateReport = "ERROR_DUPLICATE_REPORT"
+errorCode InvalidPingbackIp = "ERROR_IP_ADDRES"
+errorCode TokenExpired = "ERROR_TOKEN_EXPIRED"
+errorCode EmptyAction = "ERROR_EMPTY_ACTION"
+errorCode ProxyConnectionFailed = "ERROR_PROXY_CONNECTION_FAILED"
+
+-- | Parser instance for parsing an error code to a 'TwoCaptchaErrorCode'.
+errorParser :: Parser TwoCaptchaErrorCode
+errorParser =
+  try (string (errorCode WrongUserKey) $> WrongUserKey)
+    <|> try (string (errorCode KeyDoesNotExist) $> KeyDoesNotExist)
+    <|> try (string (errorCode ZeroBalance) $> ZeroBalance)
+    <|> try (string (errorCode PageUrlMissing) $> PageUrlMissing)
+    <|> try (string (errorCode NoSlotAvailable) $> NoSlotAvailable)
+    <|> try (string (errorCode ZeroCaptchaFileSize) $> ZeroCaptchaFileSize)
+    <|> try (string (errorCode TooBigCaptchaFileSize) $> TooBigCaptchaFileSize)
+    <|> try (string (errorCode WrongFileExtension) $> WrongFileExtension)
+    <|> try (string (errorCode ImageTypeNotSupported) $> ImageTypeNotSupported)
+    <|> try (string (errorCode UploadFailure) $> UploadFailure)
+    <|> try (string (errorCode IpNotAllowed) $> IpNotAllowed)
+    <|> try (string (errorCode IpBanned) $> IpBanned)
+    <|> try (string (errorCode BadTokenOrPageUrl) $> BadTokenOrPageUrl)
+    <|> try (string (errorCode GoogleKeyInvalid) $> GoogleKeyInvalid)
+    <|> try (string (errorCode GoogleKeyMissing) $> GoogleKeyMissing)
+    <|> try (string (errorCode CaptchaImageBlocked) $> CaptchaImageBlocked)
+    <|> try (string (errorCode TooManyBadImages) $> TooManyBadImages)
+    <|> try (string (errorCode RateLimited) $> RateLimited)
+    <|> try (string (errorCode Error1001) $> Error1001)
+    <|> try (string (errorCode Error1002) $> Error1002)
+    <|> try (string (errorCode Error1003) $> Error1003)
+    <|> try (string (errorCode Error1004) $> Error1004)
+    <|> try (string (errorCode Error1005) $> Error1005)
+    <|> try (string (errorCode BadParameters) $> BadParameters)
+    <|> try (string (errorCode BadProxy) $> BadProxy)
+    <|> try (string (errorCode CaptchaNotReady) $> CaptchaNotReady)
+    <|> try (string (errorCode CaptchaUnsolvable) $> CaptchaUnsolvable)
+    <|> try (string (errorCode WrongIdFormat) $> WrongIdFormat)
+    <|> try (string (errorCode WrongCaptchaId) $> WrongCaptchaId)
+    <|> try (string (errorCode BadDuplicates) $> BadDuplicates)
+    <|> try (string (errorCode ReportNotRecorded) $> ReportNotRecorded)
+    <|> try (string (errorCode DuplicateReport) $> DuplicateReport)
+    <|> try (string (errorCode InvalidPingbackIp) $> InvalidPingbackIp)
+    <|> try (string (errorCode TokenExpired) $> TokenExpired $> TokenExpired $> TokenExpired $> TokenExpired)
+    <|> try (string (errorCode EmptyAction) $> EmptyAction)
+    <|> string (errorCode ProxyConnectionFailed) $> ProxyConnectionFailed
+
+-- | Read an error code as its corresponding 'TwoCaptchaErrorCode'.
+readErrorCode :: String -> Either ParseError TwoCaptchaErrorCode
+readErrorCode = parse errorParser ""
diff --git a/src/TwoCaptcha/Internal/Types/FunCaptcha.hs b/src/TwoCaptcha/Internal/Types/FunCaptcha.hs
new file mode 100644
--- /dev/null
+++ b/src/TwoCaptcha/Internal/Types/FunCaptcha.hs
@@ -0,0 +1,50 @@
+module TwoCaptcha.Internal.Types.FunCaptcha where
+
+import Control.Lens (Lens', (&), (?~))
+import Data.Text (Text)
+import TwoCaptcha.Internal.Types.Captcha (Captcha, HasCaptchaLenses (method), HasCommonCaptchaLenses, HasPageURL, HasProxy, defaultCaptcha, mkParamLens)
+
+-- | Parameters used to solve FunCaptcha.
+newtype FunCaptcha = MkFunCaptcha Captcha deriving (Show)
+
+instance HasCommonCaptchaLenses FunCaptcha
+
+instance HasCaptchaLenses FunCaptcha
+
+instance HasPageURL FunCaptcha
+
+instance HasProxy FunCaptcha
+
+-- |
+-- Parameters used to solve FunCaptcha.
+--
+-- Required parameters:
+--
+-- * 'TwoCaptcha.Internal.Types.Captcha.apiKey'
+-- * 'publicKey'
+-- * 'TwoCaptcha.Internal.Types.Captcha.pageUrl'
+--
+-- Optional parameters:
+--
+-- * 'surl'
+-- * 'customDataField'
+-- * 'TwoCaptcha.Internal.Types.Captcha.userAgent'
+-- * 'TwoCaptcha.Internal.Types.Captcha.headerACAO'
+-- * 'TwoCaptcha.Internal.Types.Captcha.pingback'
+-- * 'TwoCaptcha.Internal.Types.Captcha.softId'
+-- * 'TwoCaptcha.Internal.Types.Captcha.proxy'
+-- * 'TwoCaptcha.Internal.Types.Captcha.proxyType'
+funCaptcha :: FunCaptcha
+funCaptcha = MkFunCaptcha (defaultCaptcha & method ?~ "funcaptcha")
+
+-- | Value of __pk__ or __data-pkey__ found on the FunCaptcha page.
+publicKey :: Lens' FunCaptcha (Maybe Text)
+publicKey = mkParamLens "publickey"
+
+-- | Value of __surl__ found on the FunCaptcha page.
+surl :: Lens' FunCaptcha (Maybe Text)
+surl = mkParamLens "surl"
+
+-- | Custom data to pass to FunCaptcha.
+customDataField :: Text -> Lens' FunCaptcha (Maybe Text)
+customDataField field = mkParamLens ("data[" <> field <> "]")
diff --git a/src/TwoCaptcha/Internal/Types/GeeTestCaptcha.hs b/src/TwoCaptcha/Internal/Types/GeeTestCaptcha.hs
new file mode 100644
--- /dev/null
+++ b/src/TwoCaptcha/Internal/Types/GeeTestCaptcha.hs
@@ -0,0 +1,50 @@
+module TwoCaptcha.Internal.Types.GeeTestCaptcha where
+
+import Control.Lens (Lens', (&), (?~))
+import Data.Text (Text)
+import TwoCaptcha.Internal.Types.Captcha (Captcha, HasCaptchaLenses (method), HasCommonCaptchaLenses, HasPageURL, HasProxy, defaultCaptcha, mkParamLens)
+
+-- | Parameters used to solve a GeeTest captcha.
+newtype GeeTestCaptcha = MkGeeTestCaptcha Captcha deriving (Show)
+
+instance HasCommonCaptchaLenses GeeTestCaptcha
+
+instance HasCaptchaLenses GeeTestCaptcha
+
+instance HasPageURL GeeTestCaptcha
+
+instance HasProxy GeeTestCaptcha
+
+-- |
+-- Parameters used to solve a GeeTest captcha.
+--
+-- Required parameters:
+--
+-- * 'TwoCaptcha.Internal.Types.Captcha.apiKey'
+-- * 'gt'
+-- * 'challenge'
+-- * 'TwoCaptcha.Internal.Types.Captcha.pageUrl'
+--
+-- Optional parameters:
+--
+-- * 'apiServer'
+-- * 'TwoCaptcha.Internal.Types.Captcha.userAgent'
+-- * 'TwoCaptcha.Internal.Types.Captcha.headerACAO'
+-- * 'TwoCaptcha.Internal.Types.Captcha.pingback'
+-- * 'TwoCaptcha.Internal.Types.Captcha.softId'
+-- * 'TwoCaptcha.Internal.Types.Captcha.proxy'
+-- * 'TwoCaptcha.Internal.Types.Captcha.proxyType'
+geeTestCaptcha :: GeeTestCaptcha
+geeTestCaptcha = MkGeeTestCaptcha (defaultCaptcha & method ?~ "geetest")
+
+-- | Value of __gt__ parameter you found on target website.
+gt :: Lens' GeeTestCaptcha (Maybe Text)
+gt = mkParamLens "gt"
+
+-- | Value of __challenge__ parameter you found on target website.
+challenge :: Lens' GeeTestCaptcha (Maybe Text)
+challenge = mkParamLens "challenge"
+
+-- | Value of __api_server__ parameter you found on target website.
+apiServer :: Lens' GeeTestCaptcha (Maybe Text)
+apiServer = mkParamLens "api_server"
diff --git a/src/TwoCaptcha/Internal/Types/GridCaptcha.hs b/src/TwoCaptcha/Internal/Types/GridCaptcha.hs
new file mode 100644
--- /dev/null
+++ b/src/TwoCaptcha/Internal/Types/GridCaptcha.hs
@@ -0,0 +1,79 @@
+module TwoCaptcha.Internal.Types.GridCaptcha where
+
+import Control.Lens (Lens')
+import Data.Text (Text)
+import TwoCaptcha.Internal.Types.Captcha (Captcha, HasCaptchaLenses, HasCommonCaptchaLenses, HasLanguage, HasLocalImage, defaultCaptcha, mkParamLens, mkParamLens', mkParamLensBool)
+
+-- | Parameters used to solve a grid captcha.
+newtype GridCaptcha = MkGridCaptcha Captcha deriving (Show)
+
+instance HasCommonCaptchaLenses GridCaptcha
+
+instance HasCaptchaLenses GridCaptcha
+
+instance HasLocalImage GridCaptcha
+
+instance HasLanguage GridCaptcha
+
+-- |
+-- Parameters for solving a grid captcha.
+--
+-- Required parameters:
+--
+-- * 'TwoCaptcha.Internal.Types.Captcha.apiKey'
+-- * 'TwoCaptcha.Internal.Types.Captcha.method'
+-- * 'reCAPTCHAImage'
+-- * 'TwoCaptcha.Internal.Types.Captcha.file'*
+-- * 'TwoCaptcha.Internal.Types.Captcha.body'*
+--
+-- Optional parameters:
+--
+-- * 'canvas'
+-- * 'TwoCaptcha.Internal.Types.Captcha.textInstructions'
+-- * 'TwoCaptcha.Internal.Types.Captcha.imgInstructions'
+-- * 'reCAPTCHAImage'
+-- * 'rows'
+-- * 'columns'
+-- * 'previousId'
+-- * 'cannotAnswer'
+-- * 'TwoCaptcha.Internal.Types.Captcha.language'
+-- * 'TwoCaptcha.Internal.Types.Captcha.languageCode'
+-- * 'TwoCaptcha.Internal.Types.Captcha.headerACAO'
+-- * 'TwoCaptcha.Internal.Types.Captcha.pingback'
+-- * 'TwoCaptcha.Internal.Types.Captcha.softId'
+--
+-- Possible 'TwoCaptcha.Internal.Types.Captcha.method' values:
+--
+-- * __post__ - defines that you're sending an image with multipart form
+-- * __base64__  - defines that you're sending a base64 encoded image
+--
+-- Starred required parameter rules:
+--
+-- * __file__ is only required if __captcha = "post"__
+-- * __body__ is only required if __captcha = "base64"__
+gridCaptcha :: GridCaptcha
+gridCaptcha = MkGridCaptcha defaultCaptcha
+
+-- | Defines that you're sending a reCAPTCHA as an image.
+reCAPTCHAImage :: Lens' GridCaptcha (Maybe Bool)
+reCAPTCHAImage = mkParamLensBool "recaptcha"
+
+-- | Defines that you want to use a <https://2captcha.com/2captcha-api#canvas canvas> method.
+canvas :: Lens' GridCaptcha (Maybe Bool)
+canvas = mkParamLensBool "canvas"
+
+-- | Number of rows in reCAPTCHA grid.
+rows :: Lens' GridCaptcha (Maybe Int)
+rows = mkParamLens' "recaptcharows"
+
+-- | Number of columns in reCAPTCHA grid.
+columns :: Lens' GridCaptcha (Maybe Int)
+columns = mkParamLens' "recaptchacols"
+
+-- | Id of your previous request with the same captcha challenge.
+previousId :: Lens' GridCaptcha (Maybe Text)
+previousId = mkParamLens "previousID"
+
+-- | Defines if the captcha can potentially have no images, or possibly cannot be answered.
+cannotAnswer :: Lens' GridCaptcha (Maybe Bool)
+cannotAnswer = mkParamLensBool "can_no_answer"
diff --git a/src/TwoCaptcha/Internal/Types/HCaptcha.hs b/src/TwoCaptcha/Internal/Types/HCaptcha.hs
new file mode 100644
--- /dev/null
+++ b/src/TwoCaptcha/Internal/Types/HCaptcha.hs
@@ -0,0 +1,55 @@
+module TwoCaptcha.Internal.Types.HCaptcha where
+
+import Control.Lens (Lens', (&), (?~))
+import Data.Text (Text)
+import TwoCaptcha.Internal.Types.Captcha (Captcha, HasCaptchaLenses (method), HasCommonCaptchaLenses, HasPageURL, HasProxy, defaultCaptcha, mkParamLens, mkParamLensBool)
+
+-- | Parameters used to solve hCaptcha.
+newtype HCaptcha = MkHCaptcha Captcha deriving (Show)
+
+instance HasCommonCaptchaLenses HCaptcha
+
+instance HasCaptchaLenses HCaptcha
+
+instance HasPageURL HCaptcha
+
+instance HasProxy HCaptcha
+
+-- |
+-- Parameters used to solve hCaptcha.
+--
+-- Required parameters:
+--
+-- * 'TwoCaptcha.Internal.Types.Captcha.apiKey'
+-- * 'sitekey'
+-- * 'TwoCaptcha.Internal.Types.Captcha.pageUrl'
+--
+-- Optional parameters:
+--
+-- * 'hInvisible'
+-- * 'customData'
+-- * 'TwoCaptcha.Internal.Types.Captcha.userAgent'
+-- * 'TwoCaptcha.Internal.Types.Captcha.headerACAO'
+-- * 'TwoCaptcha.Internal.Types.Captcha.pingback'
+-- * 'TwoCaptcha.Internal.Types.Captcha.softId'
+-- * 'TwoCaptcha.Internal.Types.Captcha.proxy'
+-- * 'TwoCaptcha.Internal.Types.Captcha.proxyType'
+hCaptcha :: HCaptcha
+hCaptcha = MkHCaptcha (defaultCaptcha & method ?~ "hcaptcha")
+
+-- | Value of __data-sitekey__ parameter on target website.
+siteKey :: Lens' HCaptcha (Maybe Text)
+siteKey = mkParamLens "sitekey"
+
+-- | Defines if the captcha is invisible. Invisible hCaptchas are currently a rare case.
+hInvisible :: Lens' HCaptcha (Maybe Bool)
+hInvisible = mkParamLensBool "invisible"
+
+-- |
+-- Custom data that is used in some implementations of hCaptcha, mostly with invisible captchas.
+-- In most cases, you see it as __rqdata__ inside network requests.
+--
+-- __IMPORTANT__: you MUST provide 'userAgent' if you submit with 'customData'.
+-- The value should match the User-Agent you use when interacting with target website.
+customData :: Lens' HCaptcha (Maybe Text)
+customData = mkParamLens "data"
diff --git a/src/TwoCaptcha/Internal/Types/KeyCaptcha.hs b/src/TwoCaptcha/Internal/Types/KeyCaptcha.hs
new file mode 100644
--- /dev/null
+++ b/src/TwoCaptcha/Internal/Types/KeyCaptcha.hs
@@ -0,0 +1,50 @@
+module TwoCaptcha.Internal.Types.KeyCaptcha where
+
+import Control.Lens (Lens', (&), (?~))
+import Data.Text (Text)
+import TwoCaptcha.Internal.Types.Captcha (Captcha, HasCaptchaLenses (method), HasCommonCaptchaLenses, HasPageURL, defaultCaptcha, mkParamLens)
+
+-- | Parameters used to solve KeyCaptcha.
+newtype KeyCaptcha = MkKeyCaptcha Captcha deriving (Show)
+
+instance HasCommonCaptchaLenses KeyCaptcha
+
+instance HasCaptchaLenses KeyCaptcha
+
+instance HasPageURL KeyCaptcha
+
+-- |
+-- Parameters used to solve a KeyCaptcha.
+--
+-- Required parameters:
+--
+-- * 'TwoCaptcha.Internal.Types.Captcha.apiKey'
+-- * 'userId'
+-- * 'sessionId'
+-- * 'webServerSign'
+-- * 'webServerSign2'
+-- * 'TwoCaptchal.Internal.Types.Captcha.pageUrl'
+--
+-- Optional parameters:
+--
+-- * 'TwoCaptcha.Internal.Types.Captcha.headerACAO'
+-- * 'TwoCaptcha.Internal.Types.Captcha.pingback'
+-- * 'TwoCaptcha.Internal.Types.Captcha.softId'
+keyCaptcha :: KeyCaptcha
+keyCaptcha = MkKeyCaptcha (defaultCaptcha & method ?~ "keycaptcha")
+
+-- | Value of __s_s_c_user_id__ parameter you found on page.
+userId :: Lens' KeyCaptcha (Maybe Text)
+userId = mkParamLens "s_s_c_user_id"
+
+-- | Value of __s_s_c_session_id__ parameter you found on page.
+sessionId :: Lens' KeyCaptcha (Maybe Text)
+sessionId = mkParamLens "s_s_c_session_id"
+
+-- | Value of __s_s_c_web_server_sign__ parameter you found on page.
+webServerSign :: Lens' KeyCaptcha (Maybe Text)
+webServerSign = mkParamLens "s_s_c_web_server_sign"
+
+-- | Value of __s_s_c_web_server_sign2__ parameter you found on page.
+webServerSign2 :: Lens' KeyCaptcha (Maybe Text)
+webServerSign2 = mkParamLens "s_s_c_web_server_sign2"
diff --git a/src/TwoCaptcha/Internal/Types/NormalCaptcha.hs b/src/TwoCaptcha/Internal/Types/NormalCaptcha.hs
new file mode 100644
--- /dev/null
+++ b/src/TwoCaptcha/Internal/Types/NormalCaptcha.hs
@@ -0,0 +1,88 @@
+module TwoCaptcha.Internal.Types.NormalCaptcha where
+
+import Control.Lens (Lens')
+import TwoCaptcha.Internal.Types.Captcha (Captcha, HasCaptchaLenses, HasCommonCaptchaLenses, HasLanguage, HasLocalImage, defaultCaptcha, mkParamLens', mkParamLensBool)
+
+-- | Parameters used to solve a normal captcha.
+newtype NormalCaptcha = MkNormalCaptcha Captcha deriving (Show)
+
+instance HasCommonCaptchaLenses NormalCaptcha
+
+instance HasCaptchaLenses NormalCaptcha
+
+instance HasLocalImage NormalCaptcha
+
+instance HasLanguage NormalCaptcha
+
+-- |
+-- Parameters for solving a normal captcha.
+--
+-- Required parameters:
+--
+-- * 'TwoCaptcha.Internal.Types.Captcha.apiKey'
+-- * 'TwoCaptcha.Internal.Types.Captcha.method'
+-- * 'TwoCaptcha.Internal.Types.Captcha.file'*
+-- * 'TwoCaptcha.Internal.Types.Captcha.body'*
+--
+-- Optional parameters:
+--
+-- * 'phrase'
+-- * 'caseSensitive'
+-- * 'format'
+-- * 'calc'
+-- * 'minLength'
+-- * 'maxLength'
+-- * 'TwoCaptcha.Internal.Types.Captcha.language'
+-- * 'TwoCaptcha.Internal.Types.Captcha.languageCode'
+-- * 'TwoCaptcha.Internal.Types.Captcha.textInstructions'
+-- * 'TwoCaptcha.Internal.Types.Captcha.imgInstructions'
+-- * 'TwoCaptcha.Internal.Types.Captcha.headerACAO'
+-- * 'TwoCaptcha.Internal.Types.Captcha.pingback'
+-- * 'TwoCaptcha.Internal.Types.Captcha.softId'
+--
+-- Possible 'TwoCaptcha.Internal.Types.Captcha.method' values:
+--
+-- * __post__ - defines that you're sending an image with multipart form
+-- * __base64__  - defines that you're sending a base64 encoded image
+--
+-- Starred required parameter rules:
+--
+-- * __file__ is only required if __captcha = "post"__
+-- * __body__ is only required if __captcha = "base64"__
+normalCaptcha :: NormalCaptcha
+normalCaptcha = MkNormalCaptcha defaultCaptcha
+
+-- |
+-- Defines if the captcha is a phrase.
+--
+-- * True - Captcha contains two or more words.
+-- * False - Captcha contains one word.
+phrase :: Lens' NormalCaptcha (Maybe Bool)
+phrase = mkParamLensBool "phrase"
+
+-- | Defines if the captcha is case sensitive.
+caseSensitive :: Lens' NormalCaptcha (Maybe Bool)
+caseSensitive = mkParamLensBool "regsense"
+
+-- |
+-- The captcha's format:
+--
+-- 0. Not specified.
+-- 1. Captcha contains only numbers.
+-- 2. Captcha contains only letters.
+-- 3. Captcha contains only numbers OR only letters.
+-- 4. Captcha contains both numbers AND letters.
+format :: Lens' NormalCaptcha (Maybe Int)
+format = mkParamLens' "numeric"
+
+-- | Define if the captcha requires calculation (e.g. 1 + 1 = ?).
+calc :: Lens' NormalCaptcha (Maybe Bool)
+calc = mkParamLensBool "calc"
+
+-- | The minimum number of symbols in the captcha (up to 20).
+minLength :: Lens' NormalCaptcha (Maybe Int)
+minLength = mkParamLens' "min_len"
+
+-- | The maximum numbers of symbols in the captcha (up to 20).
+maxLength :: Lens' NormalCaptcha (Maybe Int)
+maxLength = mkParamLens' "max_len"
diff --git a/src/TwoCaptcha/Internal/Types/ReCaptcha.hs b/src/TwoCaptcha/Internal/Types/ReCaptcha.hs
new file mode 100644
--- /dev/null
+++ b/src/TwoCaptcha/Internal/Types/ReCaptcha.hs
@@ -0,0 +1,119 @@
+module TwoCaptcha.Internal.Types.ReCaptcha where
+
+import Control.Lens (Lens', (%~), (&), (.~), (?~))
+import Data.Text (Text)
+import GHC.Base (Coercible)
+import Network.Wreq.Lens (param)
+import TwoCaptcha.Internal.Types.Captcha (Captcha, HasCaptchaLenses (method), HasCommonCaptchaLenses, HasCookies, HasPageURL, HasProxy, HasUserAgent, TimeoutDuration, defaultCaptcha, mkParamLens, mkParamLens', mkParamLensBool, options)
+
+-- | Default parameters for solving a recaptcha. Internal use only.
+defaultReCAPTCHA :: Captcha
+defaultReCAPTCHA = defaultCaptcha & method ?~ "userrecaptcha"
+
+-- | Lenses for constructing ReCaptcha options.
+class Coercible Captcha a => HasReCaptchaLenses a where
+  -- | Defines if your ReCaptcha is enterprise.
+  enterprise :: Lens' a (Maybe Bool)
+  enterprise = mkParamLensBool "enterprise"
+
+  -- |
+  -- If using 'ReCaptchaV2', this is the value of __k__ or __data-sitekey__ found on the captcha page.
+  --
+  -- If using 'ReCaptchaV3', this is the value of __sitekey__ found on the captcha page.
+  googleKey :: Lens' a (Maybe Text)
+  googleKey = mkParamLens "googlekey"
+
+  -- | Domain used to load the captcha: __google.com__ or __recaptcha.net__.
+  domain :: Lens' a (Maybe Text)
+  domain = mkParamLens "domain"
+
+-- | Parameters used to solve reCAPTCHA V2.
+newtype ReCaptchaV2 = MkReCaptchaV2 Captcha deriving (Show)
+
+instance HasCommonCaptchaLenses ReCaptchaV2
+
+instance HasCaptchaLenses ReCaptchaV2
+
+instance HasReCaptchaLenses ReCaptchaV2
+
+instance HasPageURL ReCaptchaV2
+
+instance HasProxy ReCaptchaV2
+
+instance HasUserAgent ReCaptchaV2
+
+instance HasCookies ReCaptchaV2
+
+-- |
+-- Parameters used to solve reCAPTCHA V2.
+--
+-- Required parameters:
+--
+-- * 'TwoCaptcha.Internal.Types.Captcha.apiKey'
+-- * 'googleKey'
+-- * 'TwoCaptcha.Internal.Types.Captcha.pageUrl'
+--
+-- Optional parameters:
+--
+-- * 'enterprise'
+-- * 'domain'
+-- * 'invisible'
+-- * 'dataS'
+-- * 'TwoCaptcha.Internal.Types.Captcha.cookies'
+-- * 'TwoCaptcha.Internal.Types.Captcha.userAgent'
+-- * 'TwoCaptcha.Internal.Types.Captcha.headerACAO'
+-- * 'TwoCaptcha.Internal.Types.Captcha.pingback'
+-- * 'TwoCaptcha.Internal.Types.Captcha.softId'
+-- * 'TwoCaptcha.Internal.Types.Captcha.proxy'
+-- * 'TwoCaptcha.Internal.Types.Captcha.proxyType'
+reCAPTCHAV2 :: ReCaptchaV2
+reCAPTCHAV2 = MkReCaptchaV2 defaultReCAPTCHA
+
+-- | Defines if the reCAPTCHA v2 is invisible.
+invisible :: Lens' ReCaptchaV2 (Maybe Bool)
+invisible = mkParamLensBool "invisible"
+
+-- | Value of the __data-s__ parameter found on the reCAPTCHA page. Currently applicable for google services.
+dataS :: Lens' ReCaptchaV2 (Maybe Text)
+dataS = mkParamLens "data-s"
+
+-- | Parameters used to solve reCAPTCHA V3.
+newtype ReCaptchaV3 = MkReCaptchaV3 Captcha deriving (Show)
+
+instance HasCommonCaptchaLenses ReCaptchaV3
+
+instance HasCaptchaLenses ReCaptchaV3
+
+instance HasReCaptchaLenses ReCaptchaV3
+
+instance HasPageURL ReCaptchaV3
+
+-- |
+-- Parameters used to solve reCAPTCHA V3.
+--
+-- Required parameters:
+--
+-- * 'TwoCaptcha.Internal.Types.Captcha.apiKey'
+-- * 'googleKey'
+-- * 'TwoCaptcha.Internal.Types.Captcha.pageUrl'
+--
+-- Optional parameters:
+--
+-- * 'enterprise'
+-- * 'domain'
+-- * 'minScore'
+-- * 'TwoCaptcha.Internal.Types.Captcha.headerACAO'
+-- * 'TwoCaptcha.Internal.Types.Captcha.pingback'
+-- * 'TwoCaptcha.Internal.Types.Captcha.softId'
+-- * 'TwoCaptcha.Internal.Types.Captcha.proxy'
+-- * 'TwoCaptcha.Internal.Types.Captcha.proxyType'
+reCAPTCHAV3 :: ReCaptchaV3
+reCAPTCHAV3 = MkReCaptchaV3 (defaultReCAPTCHA & options %~ (& param "version" .~ ["v3"]))
+
+-- | The score needed for resolution. Currently it's almost impossible to get a token with a score higher than 0.3
+minScore :: Lens' ReCaptchaV3 (Maybe Double)
+minScore = mkParamLens' "min_score"
+
+-- | Default reCAPTCHA timeout duration (600 seconds).
+reCAPTCHATimeout :: TimeoutDuration
+reCAPTCHATimeout = 600000
diff --git a/src/TwoCaptcha/Internal/Types/RotateCaptcha.hs b/src/TwoCaptcha/Internal/Types/RotateCaptcha.hs
new file mode 100644
--- /dev/null
+++ b/src/TwoCaptcha/Internal/Types/RotateCaptcha.hs
@@ -0,0 +1,40 @@
+module TwoCaptcha.Internal.Types.RotateCaptcha where
+
+import Control.Lens (Lens', (&), (?~))
+import TwoCaptcha.Internal.Types.Captcha (Captcha, HasCaptchaLenses (method), HasCommonCaptchaLenses, HasLocalImage, defaultCaptcha, mkParamLens')
+
+-- | Parameters used to solve a rotate captcha.
+newtype RotateCaptcha = MkRotateCaptcha Captcha deriving (Show)
+
+instance HasCommonCaptchaLenses RotateCaptcha
+
+instance HasCaptchaLenses RotateCaptcha
+
+instance HasLocalImage RotateCaptcha
+
+-- |
+-- Parameters used to solve a rotate captcha.
+--
+-- Required parameters:
+--
+-- * 'TwoCaptcha.Internal.Types.Captcha.apiKey'
+-- * 'TwoCaptcha.Internal.Types.Captcha.file'*
+-- * 'TwoCaptcha.Internal.Types.Captcha.body'*
+--
+-- Optional parameters:
+--
+-- * 'angle'
+-- * 'TwoCaptcha.Internal.Types.Captcha.headerACAO'
+-- * 'TwoCaptcha.Internal.Types.Captcha.pingback'
+-- * 'TwoCaptcha.Internal.Types.Captcha.softId'
+--
+-- Starred required parameter rules:
+--
+-- * __file__ is only required if your captcha is sent as a file.
+-- * __body__ is only required if your captcha is sent in base64 format.
+rotateCaptcha :: RotateCaptcha
+rotateCaptcha = MkRotateCaptcha (defaultCaptcha & method ?~ "rotatecaptcha")
+
+-- | Angle for one rotation step in degrees. Defaults to 40 degrees if not specified.
+angle :: Lens' RotateCaptcha (Maybe Int)
+angle = mkParamLens' "angle"
diff --git a/src/TwoCaptcha/Internal/Types/TextCaptcha.hs b/src/TwoCaptcha/Internal/Types/TextCaptcha.hs
new file mode 100644
--- /dev/null
+++ b/src/TwoCaptcha/Internal/Types/TextCaptcha.hs
@@ -0,0 +1,36 @@
+module TwoCaptcha.Internal.Types.TextCaptcha where
+
+import Control.Lens (Lens')
+import Data.Text (Text)
+import TwoCaptcha.Internal.Types.Captcha (Captcha, HasCaptchaLenses, HasCommonCaptchaLenses, HasLanguage, defaultCaptcha, mkParamLens)
+
+-- | Parameters used to solve a text captcha.
+newtype TextCaptcha = MkTextCaptcha Captcha deriving (Show)
+
+instance HasCommonCaptchaLenses TextCaptcha
+
+instance HasCaptchaLenses TextCaptcha
+
+instance HasLanguage TextCaptcha
+
+-- |
+-- Parameters for solving a text captcha.
+--
+-- Required parameters:
+--
+-- * 'TwoCaptcha.Internal.Types.Captcha.apiKey'
+-- * 'textContent'
+--
+-- Optional parameters:
+--
+-- * 'TwoCaptcha.Internal.Types.Captcha.language'
+-- * 'TwoCaptcha.Internal.Types.Captcha.languageCode'
+-- * 'TwoCaptcha.Internal.Types.Captcha.headerACAO'
+-- * 'TwoCaptcha.Internal.Types.Captcha.pingback'
+-- * 'TwoCaptcha.Internal.Types.Captcha.softId'
+textCaptcha :: TextCaptcha
+textCaptcha = MkTextCaptcha defaultCaptcha
+
+-- | The text captcha's content.
+textContent :: Lens' TextCaptcha (Maybe Text)
+textContent = mkParamLens "textcaptcha"
diff --git a/src/TwoCaptcha/Internal/Types/TikTokCaptcha.hs b/src/TwoCaptcha/Internal/Types/TikTokCaptcha.hs
new file mode 100644
--- /dev/null
+++ b/src/TwoCaptcha/Internal/Types/TikTokCaptcha.hs
@@ -0,0 +1,45 @@
+module TwoCaptcha.Internal.Types.TikTokCaptcha where
+
+import Control.Lens (Lens', (&), (?~))
+import Data.Text (Text)
+import TwoCaptcha.Internal.Types.Captcha (Captcha, HasCaptchaLenses (method), HasCommonCaptchaLenses, HasCookies, HasPageURL, HasProxy, defaultCaptcha, mkParamLens, mkParamLens')
+
+-- | Parameters used to solve a TikTok captcha.
+newtype TikTokCaptcha = MkTikTokCaptcha Captcha deriving (Show)
+
+instance HasCommonCaptchaLenses TikTokCaptcha
+
+instance HasCaptchaLenses TikTokCaptcha
+
+instance HasPageURL TikTokCaptcha
+
+instance HasProxy TikTokCaptcha
+
+instance HasCookies TikTokCaptcha
+
+-- |
+-- Parameters used to solve a TikTok captcha.
+--
+-- Required parameters:
+--
+-- * 'TwoCaptcha.Internal.Types.Captcha.apiKey'
+-- * 'TwoCaptcha.Internal.Types.Captcha.cookies'
+-- * 'aid'
+-- * 'host'
+-- * 'TwoCaptcha.Internal.Types.Captcha.pageUrl'
+--
+-- Optional parameters:
+--
+-- * 'TwoCaptcha.Internal.Types.Captcha.softId'
+-- * 'TwoCaptcha.Internal.Types.Captcha.proxy'
+-- * 'TwoCaptcha.Internal.Types.Captcha.proxyType'
+tikTokCaptcha :: TikTokCaptcha
+tikTokCaptcha = MkTikTokCaptcha (defaultCaptcha & method ?~ "tiktok")
+
+-- | The __aid__ parameter value found on the page.
+aid :: Lens' TikTokCaptcha (Maybe Int)
+aid = mkParamLens' "aid"
+
+-- | The __host__ parameter value found on the page.
+host :: Lens' TikTokCaptcha (Maybe Text)
+host = mkParamLens "host"
