diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+# 2.1.1.0
+* [#145](https://github.com/MercuryTechnologies/slack-web/pull/145)
+  Implement `conversations.info` API method.
+
+  Breaking change: imIsUserDeleted is now Maybe to reflect reality.
+* [#146](https://github.com/MercuryTechnologies/slack-web/pull/146)
+  Implement `conversations.join` API method.
+
 # 2.1.0.0 (2025-03-06)
 * [#138](https://github.com/MercuryTechnologies/slack-web/pull/138)
   Implement `views.publish` method and App Home tab events.
diff --git a/slack-web.cabal b/slack-web.cabal
--- a/slack-web.cabal
+++ b/slack-web.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.0
 name: slack-web
-version: 2.1.0.0
+version: 2.2.0.0
 
 build-type: Simple
 
@@ -35,6 +35,10 @@
   tests/golden/PublishResp/*.golden
   tests/golden/ResponseJSON/*.json
   tests/golden/ResponseJSON/*.golden
+  tests/golden/InfoRsp/*.golden
+  tests/golden/InfoRsp/*.json
+  tests/golden/JoinRsp/*.json
+  tests/golden/JoinRsp/*.golden
 
 category: Web
 
diff --git a/src/Web/Slack/Conversation.hs b/src/Web/Slack/Conversation.hs
--- a/src/Web/Slack/Conversation.hs
+++ b/src/Web/Slack/Conversation.hs
@@ -5,6 +5,7 @@
 {-# LANGUAGE RecordWildCards #-}
 {-# LANGUAGE StrictData #-}
 {-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeOperators #-}
 -- FIXME: squashes warnings
 {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}
 
@@ -34,6 +35,15 @@
   RepliesReq (..),
   mkRepliesReq,
   ResponseMetadata (..),
+  Api,
+  InfoReq (..),
+  InfoRsp (..),
+  conversationsInfo,
+  conversationsInfo_,
+  JoinReq (..),
+  JoinRsp (..),
+  conversationsJoin,
+  conversationsJoin_,
 ) where
 
 import Data.Aeson
@@ -43,9 +53,14 @@
 import Data.Aeson.Types
 import Data.Scientific
 import Data.Text qualified as T
+import Servant.API (AuthProtect, FormUrlEncoded, JSON, Post, ReqBody, (:<|>) (..), (:>))
+import Servant.Client (ClientM, client)
+import Servant.Client.Core (AuthenticatedRequest)
 import Web.FormUrlEncoded
 import Web.HttpApiData
 import Web.Slack.Common
+import Web.Slack.Internal (ResponseJSON (..), SlackConfig (..), mkSlackAuthenticateReq, run)
+import Web.Slack.Pager (Response)
 import Web.Slack.Pager.Types (PagedRequest (..), PagedResponse (..), ResponseMetadata (..))
 import Web.Slack.Prelude
 import Web.Slack.Util
@@ -118,7 +133,7 @@
 $(deriveJSON (jsonOpts "channel") ''ChannelConversation)
 
 -- | Conversation object representing a private channel or
---   _a multi-party instant message (mpim)*, which only invited people in the
+--   _a multi-party instant message (mpim)_, which only invited people in the
 --  team can join in and see.
 data GroupConversation = GroupConversation
   { groupId :: ConversationId
@@ -171,7 +186,7 @@
   , imIsArchived :: Bool
   , imIsOrgShared :: Bool
   , imUser :: UserId
-  , imIsUserDeleted :: Bool
+  , imIsUserDeleted :: Maybe Bool
   , imPriority :: Scientific
   }
   deriving stock (Eq, Show, Generic)
@@ -429,3 +444,143 @@
     , repliesReqOldest = Nothing
     , repliesReqInclusive = True
     }
+
+-- | @conversations.info@ request: retrieve a conversation's metadata.
+--
+-- <https://api.slack.com/methods/conversations.info>
+--
+-- @since 2.2.0.0
+data InfoReq = InfoReq
+  { infoReqChannel :: ConversationId
+  , infoReqIncludeLocale :: Maybe Bool
+  , infoReqIncludeNumMembers :: Maybe Bool
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToForm InfoReq where
+  toForm InfoReq {..} =
+    [("channel", unConversationId infoReqChannel)]
+      <> toQueryParamIfJust "include_locale" infoReqIncludeLocale
+      <> toQueryParamIfJust "include_num_members" infoReqIncludeNumMembers
+
+-- | @conversations.info@ response
+--
+-- <https://api.slack.com/methods/conversations.info>
+--
+-- @since 2.2.0.0
+data InfoRsp = InfoRsp
+  { infoRspChannel :: Conversation
+  }
+  deriving stock (Eq, Show, Generic)
+
+$(deriveJSON (jsonOpts "infoRsp") ''InfoRsp)
+
+-- | FIXME(jadel): move the rest of the Conversations API into here since the old "shoving all the API in one spot" is soft deprecated.
+-- @since 2.2.0.0
+type Api =
+  "conversations.info"
+    :> AuthProtect "token"
+    :> ReqBody '[FormUrlEncoded] InfoReq
+    :> Post '[JSON] (ResponseJSON InfoRsp)
+    :<|> "conversations.join"
+      :> AuthProtect "token"
+      :> ReqBody '[FormUrlEncoded] JoinReq
+      :> Post '[JSON] (ResponseJSON JoinRsp)
+
+-- | Joins a conversation.
+--
+-- <https://api.slack.com/methods/conversations.join>
+data JoinReq = JoinReq
+  { joinReqChannel :: ConversationId
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToForm JoinReq where
+  toForm = genericToForm (formOpts "joinReq")
+
+-- |
+-- @
+-- {
+--     "ok": true,
+--     "channel": {
+--         "id": "C061EG9SL",
+--         "name": "general",
+--         "is_channel": true,
+--         "is_group": false,
+--         "is_im": false,
+--         "created": 1449252889,
+--         "creator": "U061F7AUR",
+--         "is_archived": false,
+--         "is_general": true,
+--         "unlinked": 0,
+--         "name_normalized": "general",
+--         "is_shared": false,
+--         "is_ext_shared": false,
+--         "is_org_shared": false,
+--         "pending_shared": [],
+--         "is_pending_ext_shared": false,
+--         "is_member": true,
+--         "is_private": false,
+--         "is_mpim": false,
+--         "topic": {
+--             "value": "Which widget do you worry about?",
+--             "creator": "",
+--             "last_set": 0
+--         },
+--         "purpose": {
+--             "value": "For widget discussion",
+--             "creator": "",
+--             "last_set": 0
+--         },
+--         "previous_names": []
+--     },
+--     "warning": "already_in_channel",
+--     "response_metadata": {
+--         "warnings": [
+--             "already_in_channel"
+--         ]
+--     }
+-- }
+-- @
+data JoinRsp = JoinRsp
+  { joinRspChannel :: Conversation
+  }
+  deriving stock (Show)
+
+$(deriveFromJSON (jsonOpts "joinRsp") ''JoinRsp)
+
+-- | @since 2.2.0.0
+conversationsJoin_ :: AuthenticatedRequest (AuthProtect "token") -> JoinReq -> ClientM (ResponseJSON JoinRsp)
+
+-- | @since 2.2.0.0
+conversationsInfo_ ::
+  AuthenticatedRequest (AuthProtect "token") ->
+  InfoReq ->
+  ClientM (ResponseJSON InfoRsp)
+conversationsInfo_ :<|> conversationsJoin_ = client (Proxy @Api)
+
+-- | Retrieve a conversation's metadata.
+--
+-- <https://api.slack.com/methods/conversations.info>
+--
+-- @since 2.2.0.0
+conversationsInfo ::
+  SlackConfig ->
+  InfoReq ->
+  IO (Response InfoRsp)
+conversationsInfo = flip $ \listReq -> do
+  authR <- mkSlackAuthenticateReq
+  run (conversationsInfo_ authR listReq) . slackConfigManager
+
+-- | Joins a conversation.
+--
+-- <https://api.slack.com/methods/conversations.join>
+--
+-- @since 2.2.0.0
+conversationsJoin ::
+  SlackConfig ->
+  JoinReq ->
+  IO (Response JoinRsp)
+conversationsJoin slackConfig req = do
+  let authR = mkSlackAuthenticateReq slackConfig
+  run (conversationsJoin_ authR req) . slackConfigManager $ slackConfig
diff --git a/src/Web/Slack/Internal.hs b/src/Web/Slack/Internal.hs
--- a/src/Web/Slack/Internal.hs
+++ b/src/Web/Slack/Internal.hs
@@ -23,6 +23,8 @@
 newtype ResponseJSON a = ResponseJSON (Either Common.ResponseSlackError a)
   deriving stock (Show)
 
+type role ResponseJSON representational
+
 instance (FromJSON a) => FromJSON (ResponseJSON a) where
   parseJSON = withObject "Response" $ \o -> do
     ok <- o .: "ok"
diff --git a/src/Web/Slack/Pager.hs b/src/Web/Slack/Pager.hs
--- a/src/Web/Slack/Pager.hs
+++ b/src/Web/Slack/Pager.hs
@@ -6,8 +6,8 @@
   module Web.Slack.Pager.Types,
 ) where
 
+import Data.Kind (Type)
 import Web.Slack.Common qualified as Common
-import Web.Slack.Conversation qualified as Conversation
 import Web.Slack.Pager.Types
 import Web.Slack.Prelude
 
@@ -19,6 +19,8 @@
 --   If there is no more response, the action returns an empty list.
 type LoadPage m a = m (Response [a])
 
+type LoadPage :: forall {k}. (Type -> k) -> Type -> k
+
 -- | Utility function for 'LoadPage'. Perform the 'LoadPage' action to call
 --   the function with the loaded page, until an empty page is loaded.
 loadingPage :: (Monad m, Monoid n) => LoadPage m a -> (Response [a] -> m n) -> m n
@@ -46,7 +48,7 @@
 
   let requestFromCursor cursor = setCursor cursor initialRequest
       collectAndUpdateCursor resp = do
-        let newCursor = Conversation.responseMetadataNextCursor =<< getResponseMetadata resp
+        let newCursor = responseMetadataNextCursor =<< getResponseMetadata resp
             -- emptyCursor is used for the marker to show that there are no more pages.
             cursorToSave = if isNothing newCursor then emptyCursor else newCursor
         writeIORef cursorRef cursorToSave
diff --git a/tests/Web/Slack/ConversationSpec.hs b/tests/Web/Slack/ConversationSpec.hs
--- a/tests/Web/Slack/ConversationSpec.hs
+++ b/tests/Web/Slack/ConversationSpec.hs
@@ -114,6 +114,8 @@
 
   describe "Golden tests" $ do
     mapM_ (oneGoldenTestDecode @Conversation) ["shared_channel"]
+    mapM_ (oneGoldenTestDecode @InfoRsp) ["general"]
+    mapM_ (oneGoldenTestDecode @JoinRsp) ["test"]
 
   it "errors accurately if no variant matches" $ do
     let badData =
diff --git a/tests/golden/InfoRsp/general.golden b/tests/golden/InfoRsp/general.golden
new file mode 100644
--- /dev/null
+++ b/tests/golden/InfoRsp/general.golden
@@ -0,0 +1,37 @@
+InfoRsp
+    { infoRspChannel = Channel
+        ( ChannelConversation
+            { channelId = ConversationId
+                { unConversationId = "C043YJGBY49" }
+            , channelName = "general"
+            , channelCreated = 1663890553
+            , channelIsArchived = False
+            , channelIsGeneral = True
+            , channelUnlinked = 0
+            , channelNameNormalized = "general"
+            , channelIsShared = False
+            , channelCreator = UserId
+                { unUserId = "U043H11ES4V" }
+            , channelIsExtShared = False
+            , channelIsOrgShared = False
+            , channelSharedTeamIds = Just
+                [ TeamId
+                    { unTeamId = "T043DB835ML" }
+                ]
+            , channelIsPendingExtShared = False
+            , channelIsMember = Just True
+            , channelTopic = Topic
+                { topicValue = ""
+                , topicCreator = ""
+                , topicLastSet = 0
+                }
+            , channelPurpose = Purpose
+                { purposeValue = "This is the one channel that will always include everyone. It’s a great spot for announcements and team-wide conversations."
+                , purposeCreator = "U043H11ES4V"
+                , purposeLastSet = 1663890553
+                }
+            , channelPreviousNames = []
+            , channelNumMembers = Nothing
+            }
+        )
+    }
diff --git a/tests/golden/InfoRsp/general.json b/tests/golden/InfoRsp/general.json
new file mode 100644
--- /dev/null
+++ b/tests/golden/InfoRsp/general.json
@@ -0,0 +1,49 @@
+{
+    "ok": true,
+    "channel": {
+        "id": "C043YJGBY49",
+        "name": "general",
+        "is_channel": true,
+        "is_group": false,
+        "is_im": false,
+        "is_mpim": false,
+        "is_private": false,
+        "created": 1663890553,
+        "is_archived": false,
+        "is_general": true,
+        "unlinked": 0,
+        "name_normalized": "general",
+        "is_shared": false,
+        "is_org_shared": false,
+        "is_pending_ext_shared": false,
+        "pending_shared": [],
+        "context_team_id": "T043DB835ML",
+        "updated": 1740098274213,
+        "parent_conversation": null,
+        "creator": "U043H11ES4V",
+        "is_read_only": false,
+        "is_thread_only": false,
+        "is_non_threadable": false,
+        "is_ext_shared": false,
+        "shared_team_ids": [
+            "T043DB835ML"
+        ],
+        "pending_connected_team_ids": [],
+        "is_member": true,
+        "last_read": "1740098745.617639",
+        "topic": {
+            "value": "",
+            "creator": "",
+            "last_set": 0
+        },
+        "purpose": {
+            "value": "This is the one channel that will always include everyone. It’s a great spot for announcements and team-wide conversations.",
+            "creator": "U043H11ES4V",
+            "last_set": 1663890553
+        },
+        "properties": {
+            "use_case": "welcome"
+        },
+        "previous_names": []
+    }
+}
diff --git a/tests/golden/InfoRsp/im.json b/tests/golden/InfoRsp/im.json
new file mode 100644
--- /dev/null
+++ b/tests/golden/InfoRsp/im.json
@@ -0,0 +1,71 @@
+{
+    "ok": true,
+    "channel": {
+        "id": "D0442US94JD",
+        "created": 1663960005,
+        "is_archived": false,
+        "is_im": true,
+        "is_org_shared": false,
+        "context_team_id": "T043DB835ML",
+        "updated": 1740098832769,
+        "user": "U043H11ES4V",
+        "last_read": "1671145573.165309",
+        "latest": {
+            "user": "U0442US8QGH",
+            "type": "message",
+            "ts": "1741206064.642559",
+            "bot_id": "B0439P161B9",
+            "app_id": "A0442TUPHGR",
+            "text": "Done!",
+            "team": "T043DB835ML",
+            "bot_profile": {
+                "id": "B0439P161B9",
+                "deleted": false,
+                "name": "Slacklinker dev",
+                "updated": 1740097237,
+                "app_id": "A0442TUPHGR",
+                "icons": {
+                    "image_36": "https://a.slack-edge.com/80588/img/plugins/app/bot_36.png",
+                    "image_48": "https://a.slack-edge.com/80588/img/plugins/app/bot_48.png",
+                    "image_72": "https://a.slack-edge.com/80588/img/plugins/app/service_72.png"
+                },
+                "team_id": "T043DB835ML"
+            },
+            "blocks": [
+                {
+                    "type": "rich_text",
+                    "block_id": "rdhT",
+                    "elements": [
+                        {
+                            "type": "rich_text_section",
+                            "elements": [
+                                {
+                                    "type": "text",
+                                    "text": "Done!"
+                                }
+                            ]
+                        }
+                    ]
+                }
+            ]
+        },
+        "unread_count": 48,
+        "unread_count_display": 4,
+        "is_open": true,
+        "properties": {
+            "tabs": [
+                {
+                    "type": "files",
+                    "label": "",
+                    "id": "files"
+                }
+            ],
+            "tabz": [
+                {
+                    "type": "files"
+                }
+            ]
+        },
+        "priority": 0
+    }
+}
diff --git a/tests/golden/JoinRsp/test.golden b/tests/golden/JoinRsp/test.golden
new file mode 100644
--- /dev/null
+++ b/tests/golden/JoinRsp/test.golden
@@ -0,0 +1,37 @@
+JoinRsp
+    { joinRspChannel = Channel
+        ( ChannelConversation
+            { channelId = ConversationId
+                { unConversationId = "C0451SKQN72" }
+            , channelName = "new-channel-2"
+            , channelCreated = 1664407230
+            , channelIsArchived = False
+            , channelIsGeneral = False
+            , channelUnlinked = 0
+            , channelNameNormalized = "new-channel-2"
+            , channelIsShared = False
+            , channelCreator = UserId
+                { unUserId = "U043H11ES4V" }
+            , channelIsExtShared = False
+            , channelIsOrgShared = False
+            , channelSharedTeamIds = Just
+                [ TeamId
+                    { unTeamId = "T043DB835ML" }
+                ]
+            , channelIsPendingExtShared = False
+            , channelIsMember = Just True
+            , channelTopic = Topic
+                { topicValue = ""
+                , topicCreator = ""
+                , topicLastSet = 0
+                }
+            , channelPurpose = Purpose
+                { purposeValue = ""
+                , purposeCreator = ""
+                , purposeLastSet = 0
+                }
+            , channelPreviousNames = []
+            , channelNumMembers = Nothing
+            }
+        )
+    }
diff --git a/tests/golden/JoinRsp/test.json b/tests/golden/JoinRsp/test.json
new file mode 100644
--- /dev/null
+++ b/tests/golden/JoinRsp/test.json
@@ -0,0 +1,34 @@
+{
+  "ok": true,
+  "channel": {
+    "id": "C0451SKQN72",
+    "name": "new-channel-2",
+    "is_channel": true,
+    "is_group": false,
+    "is_im": false,
+    "is_mpim": false,
+    "is_private": false,
+    "created": 1664407230,
+    "is_archived": false,
+    "is_general": false,
+    "unlinked": 0,
+    "name_normalized": "new-channel-2",
+    "is_shared": false,
+    "is_org_shared": false,
+    "is_pending_ext_shared": false,
+    "pending_shared": [],
+    "context_team_id": "T043DB835ML",
+    "parent_conversation": null,
+    "creator": "U043H11ES4V",
+    "is_ext_shared": false,
+    "shared_team_ids": ["T043DB835ML"],
+    "pending_connected_team_ids": [],
+    "is_member": true,
+    "last_read": "1664407230.583859",
+    "topic": { "value": "", "creator": "", "last_set": 0 },
+    "purpose": { "value": "", "creator": "", "last_set": 0 },
+    "previous_names": [],
+    "priority": 0
+  }
+}
+
diff --git a/tests/golden/UsersConversationsResponse/im_and_channels.golden b/tests/golden/UsersConversationsResponse/im_and_channels.golden
--- a/tests/golden/UsersConversationsResponse/im_and_channels.golden
+++ b/tests/golden/UsersConversationsResponse/im_and_channels.golden
@@ -79,7 +79,7 @@
                 , imIsOrgShared = False
                 , imUser = UserId
                     { unUserId = "U043H11ES4V" }
-                , imIsUserDeleted = False
+                , imIsUserDeleted = Just False
                 , imPriority = 0.0
                 }
             )
@@ -92,7 +92,7 @@
                 , imIsOrgShared = False
                 , imUser = UserId
                     { unUserId = "USLACKBOT" }
-                , imIsUserDeleted = False
+                , imIsUserDeleted = Just False
                 , imPriority = 0.0
                 }
             )
