diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,3 +1,7 @@
+## 0.2.1.0
+### Added
+- Add OpenURI portal support.
+
 ## 0.2.0.0
 ### Changed
 - Functions now return FilePath instead of Text, where the value is always a file URI.
diff --git a/desktop-portal.cabal b/desktop-portal.cabal
--- a/desktop-portal.cabal
+++ b/desktop-portal.cabal
@@ -1,6 +1,6 @@
 cabal-version:      1.12
 name:               desktop-portal
-version:            0.2.0.0
+version:            0.2.1.0
 license:            MIT
 license-file:       LICENSE
 maintainer:         garethdanielsmith@gmail.com
@@ -24,6 +24,7 @@
         Desktop.Portal.Account
         Desktop.Portal.FileChooser
         Desktop.Portal.Notification
+        Desktop.Portal.OpenURI
         Desktop.Portal.Settings
 
     hs-source-dirs:     src
@@ -63,6 +64,7 @@
         Desktop.Portal.AccountSpec
         Desktop.Portal.FileChooserSpec
         Desktop.Portal.NotificationSpec
+        Desktop.Portal.OpenURISpec
         Desktop.Portal.SettingsSpec
         Desktop.Portal.TestUtil
         Paths_desktop_portal
diff --git a/src/Desktop/Portal.hs b/src/Desktop/Portal.hs
--- a/src/Desktop/Portal.hs
+++ b/src/Desktop/Portal.hs
@@ -22,6 +22,7 @@
     module Desktop.Portal.Account,
     module Desktop.Portal.FileChooser,
     module Desktop.Portal.Notification,
+    module Desktop.Portal.OpenURI,
   )
 where
 
@@ -29,3 +30,4 @@
 import Desktop.Portal.FileChooser
 import Desktop.Portal.Internal qualified as Internal
 import Desktop.Portal.Notification
+import Desktop.Portal.OpenURI hiding (OpenFileOptions (..), openFile, openFileOptions) -- avoid conflict with FileChooser.openFile
diff --git a/src/Desktop/Portal/OpenURI.hs b/src/Desktop/Portal/OpenURI.hs
new file mode 100644
--- /dev/null
+++ b/src/Desktop/Portal/OpenURI.hs
@@ -0,0 +1,131 @@
+module Desktop.Portal.OpenURI
+  ( --- * Open URI
+    OpenURIOptions (..),
+    openURIOptions,
+    openURI,
+
+    -- * Open File
+    OpenFileOptions (..),
+    openFileOptions,
+    openFile,
+
+    -- * Open Directory
+    OpenDirectoryOptions (..),
+    openDirectoryOptions,
+    openDirectory,
+  )
+where
+
+import DBus (InterfaceName, IsVariant (toVariant))
+import Data.Map.Strict qualified as Map
+import Data.Maybe (catMaybes, fromMaybe)
+import Data.Text (Text)
+import Data.Word (Word32)
+import Desktop.Portal.Internal (Client, Request, sendRequest)
+import Desktop.Portal.Util (toVariantPair)
+import Text.URI (URI)
+import Text.URI qualified as URI
+
+data OpenURIOptions = OpenURIOptions
+  { uri :: URI,
+    parentWindow :: Maybe Text,
+    writable :: Maybe Bool,
+    ask :: Maybe Bool,
+    activationToken :: Maybe Text
+  }
+  deriving (Eq, Show)
+
+data OpenFileOptions = OpenFileOptions
+  { fd :: Word32,
+    parentWindow :: Maybe Text,
+    writable :: Maybe Bool,
+    ask :: Maybe Bool,
+    activationToken :: Maybe Text
+  }
+  deriving (Eq, Show)
+
+data OpenDirectoryOptions = OpenDirectoryOptions
+  { fd :: Word32,
+    parentWindow :: Maybe Text,
+    activationToken :: Maybe Text
+  }
+  deriving (Eq, Show)
+
+openURIOptions ::
+  -- | The URI to open.
+  URI ->
+  OpenURIOptions
+openURIOptions uri =
+  OpenURIOptions
+    { uri,
+      parentWindow = Nothing,
+      writable = Nothing,
+      ask = Nothing,
+      activationToken = Nothing
+    }
+
+openFileOptions ::
+  -- | The file descriptor to open.
+  Word32 ->
+  OpenFileOptions
+openFileOptions fd =
+  OpenFileOptions
+    { fd,
+      parentWindow = Nothing,
+      writable = Nothing,
+      ask = Nothing,
+      activationToken = Nothing
+    }
+
+openDirectoryOptions ::
+  -- | The file descriptor to open.
+  Word32 ->
+  OpenDirectoryOptions
+openDirectoryOptions fd =
+  OpenDirectoryOptions
+    { fd,
+      parentWindow = Nothing,
+      activationToken = Nothing
+    }
+
+openURIInterface :: InterfaceName
+openURIInterface = "org.freedesktop.portal.OpenURI"
+
+openURI :: Client -> OpenURIOptions -> IO (Request ())
+openURI client options =
+  sendRequest client openURIInterface "OpenURI" args optionsArg parseUnitResponse
+  where
+    args = [DBus.toVariant parentWindow, DBus.toVariant (URI.render options.uri)]
+    parentWindow = fromMaybe "" options.parentWindow
+    optionsArg =
+      Map.fromList . catMaybes $
+        [ toVariantPair "writable" options.writable,
+          toVariantPair "ask" options.ask,
+          toVariantPair "activation_token" options.activationToken
+        ]
+
+openFile :: Client -> OpenFileOptions -> IO (Request ())
+openFile client options =
+  sendRequest client openURIInterface "OpenFile" args optionsArg parseUnitResponse
+  where
+    args = [DBus.toVariant parentWindow, DBus.toVariant options.fd]
+    parentWindow = fromMaybe "" options.parentWindow
+    optionsArg =
+      Map.fromList . catMaybes $
+        [ toVariantPair "writable" options.writable,
+          toVariantPair "ask" options.ask,
+          toVariantPair "activation_token" options.activationToken
+        ]
+
+openDirectory :: Client -> OpenDirectoryOptions -> IO (Request ())
+openDirectory client options =
+  sendRequest client openURIInterface "OpenDirectory" args optionsArg parseUnitResponse
+  where
+    args = [DBus.toVariant parentWindow, DBus.toVariant options.fd]
+    parentWindow = fromMaybe "" options.parentWindow
+    optionsArg =
+      Map.fromList . catMaybes $
+        [toVariantPair "activation_token" options.activationToken]
+
+parseUnitResponse :: a -> IO ()
+parseUnitResponse = const (pure ())
diff --git a/test/Desktop/Portal/OpenURISpec.hs b/test/Desktop/Portal/OpenURISpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Desktop/Portal/OpenURISpec.hs
@@ -0,0 +1,124 @@
+{-# LANGUAGE QuasiQuotes #-}
+
+module Desktop.Portal.OpenURISpec (spec) where
+
+import Control.Monad (void)
+import DBus (InterfaceName, IsVariant (toVariant))
+import Data.Word (Word32)
+import Desktop.Portal qualified as Portal
+import Desktop.Portal.OpenURI (OpenDirectoryOptions (..), OpenFileOptions (..), OpenURIOptions (..))
+import Desktop.Portal.OpenURI qualified as OpenURI
+import Desktop.Portal.TestUtil
+import Test.Hspec (Spec, around, describe, it, shouldBe, shouldReturn)
+import Text.URI.QQ (uri)
+
+openURIInterface :: InterfaceName
+openURIInterface = "org.freedesktop.portal.OpenURI"
+
+spec :: Spec
+spec = do
+  around withTestBus $ do
+    describe "openURI" $ do
+      it "should encode request with all Nothings" $ \handle -> do
+        body <- savingRequestArguments handle openURIInterface "OpenURI" $ do
+          void (Portal.openURI (client handle) (Portal.openURIOptions [uri|https://example.com|]))
+        body
+          `shouldBe` [ toVariantText "",
+                       toVariantText "https://example.com",
+                       toVariantMap []
+                     ]
+
+      it "should encode request with all Justs" $ \handle -> do
+        body <- savingRequestArguments handle openURIInterface "OpenURI" $ do
+          void . Portal.openURI (client handle) $
+            OpenURIOptions
+              { uri = [uri|https://example.com|],
+                parentWindow = Just "_window",
+                writable = Just True,
+                ask = Just True,
+                activationToken = Just "_token"
+              }
+        body
+          `shouldBe` [ toVariantText "_window",
+                       toVariantText "https://example.com",
+                       toVariantMap
+                         [ ("writable", toVariant True),
+                           ("ask", toVariant True),
+                           ("activation_token", toVariantText "_token")
+                         ]
+                     ]
+
+      it "should decode response" $ \handle -> do
+        withRequestResponse handle openURIInterface "OpenURI" (successResponse []) $ do
+          (Portal.openURI (client handle) (Portal.openURIOptions [uri|https://example.com|]) >>= Portal.await)
+            `shouldReturn` Just ()
+
+    describe "openFile" $ do
+      it "should encode request with all Nothings" $ \handle -> do
+        body <- savingRequestArguments handle openURIInterface "OpenFile" $ do
+          void (OpenURI.openFile (client handle) (OpenURI.openFileOptions 42))
+        body
+          `shouldBe` [ toVariantText "",
+                       toVariant (42 :: Word32),
+                       toVariantMap []
+                     ]
+      it "should encode request with all Justs" $ \handle -> do
+        body <- savingRequestArguments handle openURIInterface "OpenFile" $ do
+          void
+            ( OpenURI.openFile
+                (client handle)
+                ( OpenFileOptions
+                    { fd = 43,
+                      parentWindow = Just "_window",
+                      writable = Just True,
+                      ask = Just True,
+                      activationToken = Just "_token"
+                    }
+                )
+            )
+        body
+          `shouldBe` [ toVariantText "_window",
+                       toVariant (43 :: Word32),
+                       toVariantMap
+                         [ ("writable", toVariant True),
+                           ("ask", toVariant True),
+                           ("activation_token", toVariantText "_token")
+                         ]
+                     ]
+
+      it "should decode response" $ \handle -> do
+        withRequestResponse handle openURIInterface "OpenFile" (successResponse []) $ do
+          (OpenURI.openFile (client handle) (OpenURI.openFileOptions 44) >>= Portal.await)
+            `shouldReturn` Just ()
+
+    describe "openDirectory" $ do
+      it "should encode request with all Nothings" $ \handle -> do
+        body <- savingRequestArguments handle openURIInterface "OpenDirectory" $ do
+          void (Portal.openDirectory (client handle) (Portal.openDirectoryOptions 42))
+        body
+          `shouldBe` [ toVariantText "",
+                       toVariant (42 :: Word32),
+                       toVariantMap []
+                     ]
+      it "should encode request with all Justs" $ \handle -> do
+        body <- savingRequestArguments handle openURIInterface "OpenDirectory" $ do
+          void
+            ( Portal.openDirectory
+                (client handle)
+                ( OpenDirectoryOptions
+                    { fd = 43,
+                      parentWindow = Just "_window",
+                      activationToken = Just "_token"
+                    }
+                )
+            )
+        body
+          `shouldBe` [ toVariantText "_window",
+                       toVariant (43 :: Word32),
+                       toVariantMap [("activation_token", toVariantText "_token")]
+                     ]
+
+      it "should decode response" $ \handle -> do
+        withRequestResponse handle openURIInterface "OpenDirectory" (successResponse []) $ do
+          (Portal.openDirectory (client handle) (Portal.openDirectoryOptions 44) >>= Portal.await)
+            `shouldReturn` Just ()
