diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,3 +1,7 @@
+## 0.3.1.0
+### Added
+- Add Secret Portal support.
+
 ## 0.3.0.0
 ### Fixed
 - The OpenURI portal now supports OpenFile and OpenDirectory correctly. This uses the file descriptor support added with the Documents portal support.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # Haskell Desktop Portal
 
-A Haskell wrapper for the [XDG Desktop Portal](https://github.com/flatpak/xdg-desktop-portal) DBUS API. Primarily intended to support applications packaged as Flatpaks (see [monomer-flatpak-example](https://github.com/Dretch/monomer-flatpak-example)).
+A Haskell wrapper for the [XDG Desktop Portal](https://github.com/flatpak/xdg-desktop-portal) DBUS API. Like [libportal](https://github.com/flatpak/libportal), but written in Haskell. Primarily intended to support applications packaged as Flatpaks (see [monomer-flatpak-example](https://github.com/Dretch/monomer-flatpak-example)).
 
 ## Current Status
 - Unstable. Functionality and API may change considerably.
@@ -12,6 +12,10 @@
 ## API Documentation
 
 See the generated docs on [Hackage](https://hackage.haskell.org/package/desktop-portal).
+
+## Example Code
+
+The [monomer-flatpak-example](https://github.com/Dretch/monomer-flatpak-example) app includes example code for many of the APIs.
 
 ## Development Guide
 
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.3.0.0
+version:            0.3.1.0
 license:            MIT
 license-file:       LICENSE
 maintainer:         garethdanielsmith@gmail.com
@@ -27,6 +27,7 @@
         Desktop.Portal.FileChooser
         Desktop.Portal.Notification
         Desktop.Portal.OpenURI
+        Desktop.Portal.Secret
         Desktop.Portal.Settings
 
     hs-source-dirs:     src
@@ -56,6 +57,7 @@
         dbus >=1.3.0 && <2,
         directory <1.4,
         modern-uri <0.4,
+        network <3.2,
         random <1.3,
         text <2.1,
         unix <2.8
@@ -71,6 +73,7 @@
         Desktop.Portal.FileChooserSpec
         Desktop.Portal.NotificationSpec
         Desktop.Portal.OpenURISpec
+        Desktop.Portal.SecretSpec
         Desktop.Portal.SettingsSpec
         Desktop.Portal.TestUtil
         Paths_desktop_portal
@@ -100,6 +103,7 @@
         hspec >=2 && <3,
         hspec-expectations <0.9,
         modern-uri <0.4,
+        network <3.2,
         process <1.7,
         random <1.3,
         temporary <1.4,
diff --git a/src/Desktop/Portal.hs b/src/Desktop/Portal.hs
--- a/src/Desktop/Portal.hs
+++ b/src/Desktop/Portal.hs
@@ -27,6 +27,7 @@
     module Desktop.Portal.FileChooser,
     module Desktop.Portal.Notification,
     module Desktop.Portal.OpenURI,
+    module Desktop.Portal.Secret,
   )
 where
 
@@ -36,3 +37,4 @@
 import Desktop.Portal.Internal qualified as Internal
 import Desktop.Portal.Notification
 import Desktop.Portal.OpenURI hiding (OpenFileOptions (..), openFile, openFileOptions) -- avoid conflict with FileChooser.openFile
+import Desktop.Portal.Secret
diff --git a/src/Desktop/Portal/Secret.hs b/src/Desktop/Portal/Secret.hs
new file mode 100644
--- /dev/null
+++ b/src/Desktop/Portal/Secret.hs
@@ -0,0 +1,32 @@
+module Desktop.Portal.Secret (retrieveSecret) where
+
+import Control.Exception (finally, throwIO)
+import DBus (InterfaceName, IsVariant (toVariant))
+import DBus.Client qualified as DBus
+import Data.ByteString (ByteString)
+import Desktop.Portal.Internal (Client, await, sendRequest)
+import Network.Socket (Family (..), SocketType (..), close, defaultProtocol, socketPair, withFdSocket)
+import Network.Socket.ByteString (recv)
+import System.Posix (Fd (..))
+
+secretsInterface :: InterfaceName
+secretsInterface = "org.freedesktop.portal.Secret"
+
+-- | Retrieve the application-specific secret.
+--
+-- Currently works in Gnome, but not KDE (see https://bugs.kde.org/show_bug.cgi?id=466197 ).
+-- The token parameter that is documented in the portal API specs is not supported either, as
+-- it is not clear exactly how this should work and it does not seem to be supported by Gnome.
+retrieveSecret :: Client -> IO ByteString
+retrieveSecret client = do
+  (r, w) <- socketPair AF_UNIX Stream defaultProtocol
+  flip finally (close r) $ do
+    flip finally (close w) $ do
+      withFdSocket w $ \fdInt -> do
+        let fd = toVariant (Fd fdInt)
+        req <- sendRequest client secretsInterface "RetrieveSecret" [fd] mempty pure
+        await req >>= \case
+          Nothing ->
+            throwIO . DBus.clientError $ "retrieveSecret: request was cancelled."
+          Just _tokens -> do
+            recv r 4096
diff --git a/test/Desktop/Portal/SecretSpec.hs b/test/Desktop/Portal/SecretSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Desktop/Portal/SecretSpec.hs
@@ -0,0 +1,58 @@
+module Desktop.Portal.SecretSpec (spec) where
+
+import Control.Concurrent (forkIO, threadDelay)
+import Control.Monad (void)
+import DBus (InterfaceName, fromVariant, methodCallBody)
+import Desktop.Portal qualified as Portal
+import Desktop.Portal.TestUtil
+import System.Posix.ByteString (Fd, closeFd, fdWrite)
+import Test.Hspec (Spec, around, describe, it, shouldBe)
+import Test.Hspec.Expectations (shouldThrow)
+
+secretsInterface :: InterfaceName
+secretsInterface = "org.freedesktop.portal.Secret"
+
+spec :: Spec
+spec = do
+  around withTestBus $ do
+    describe "retrieveSecret" $ do
+      describe "should read secret from file descriptor" $ do
+        it "when secret is written before response is returned" $ \handle -> do
+          let answer methodCall = do
+                [fromVariant -> Just (fd :: Fd), _opts] <- pure (methodCallBody methodCall)
+                void $ fdWrite fd "$secret$"
+                closeFd fd
+                pure (successResponse [])
+          withRequestAnswer handle secretsInterface "RetrieveSecret" answer $ do
+            actualSecret <- Portal.retrieveSecret (client handle)
+            actualSecret `shouldBe` "$secret$"
+
+        it "when secret is written in multiple chunks" $ \handle -> do
+          let answer methodCall = do
+                [fromVariant -> Just (fd :: Fd), _opts] <- pure (methodCallBody methodCall)
+                void $ fdWrite fd "$sec"
+                void $ fdWrite fd "ret$"
+                closeFd fd
+                pure (successResponse [])
+          withRequestAnswer handle secretsInterface "RetrieveSecret" answer $ do
+            actualSecret <- Portal.retrieveSecret (client handle)
+            actualSecret `shouldBe` "$secret$"
+
+        it "when secret is written after response is returned" $ \handle -> do
+          let answer methodCall = do
+                [fromVariant -> Just (fd :: Fd), _opts] <- pure (methodCallBody methodCall)
+                void $ forkIO $ do
+                  threadDelay 100_000
+                  void $ fdWrite fd "$secret$"
+                  closeFd fd
+                pure (successResponse [])
+          withRequestAnswer handle secretsInterface "RetrieveSecret" answer $ do
+            actualSecret <- Portal.retrieveSecret (client handle)
+            actualSecret `shouldBe` "$secret$"
+
+      it "should throw exception on request failure" $ \handle -> do
+        let answer _methodCall = do
+              pure failureResponse
+        withRequestAnswer handle secretsInterface "RetrieveSecret" answer $ do
+          Portal.retrieveSecret (client handle)
+            `shouldThrow` dbusClientException
diff --git a/test/Desktop/Portal/TestUtil.hs b/test/Desktop/Portal/TestUtil.hs
--- a/test/Desktop/Portal/TestUtil.hs
+++ b/test/Desktop/Portal/TestUtil.hs
@@ -1,5 +1,6 @@
 module Desktop.Portal.TestUtil
   ( successResponse,
+    failureResponse,
     toVariantMap,
     toVariantText,
     TestHandle,
@@ -9,6 +10,7 @@
     withMethodResponse,
     withMethodResponse_,
     withRequestResponse,
+    withRequestAnswer,
     savingMethodArguments,
     savingMethodArguments_,
     savingRequestArguments,
@@ -105,6 +107,10 @@
 
 withRequestResponse :: TestHandle -> InterfaceName -> MemberName -> [Variant] -> IO () -> IO ()
 withRequestResponse handle interfaceName methodName methodResponse cmd = do
+  withRequestAnswer handle interfaceName methodName (const (pure methodResponse)) cmd
+
+withRequestAnswer :: TestHandle -> InterfaceName -> MemberName -> (MethodCall -> IO [Variant]) -> IO () -> IO ()
+withRequestAnswer handle interfaceName methodName answer cmd = do
   export
     handle.serverClient
     portalObjectPath
@@ -116,6 +122,7 @@
   unexport handle.serverClient portalObjectPath
   where
     handleMethodCall methodCall = do
+      methodResponse <- liftIO (answer methodCall)
       emitResponseSignal handle methodCall methodResponse
       pure (ReplyReturn [toVariant (methodRequestHandle methodCall)])
 
@@ -232,6 +239,9 @@
   [ toVariant (0 :: Word32), -- success code
     toVariantMap pairs
   ]
+
+failureResponse :: [Variant]
+failureResponse = [toVariant (1 :: Word32)]
 
 toVariantMap :: [(Text, Variant)] -> Variant
 toVariantMap = toVariant . Map.fromList
