Notion API Client for Haskell
A type-safe Haskell client for the Notion API (version 2026-03-11).
Features
- Type-safe API bindings using Servant
- Comprehensive coverage of Notion API endpoints
- Support for all Notion object types: Pages, Databases, Data Sources, Blocks, Users, etc.
- Simple client interface with sensible defaults
Installation
Add to your package.yaml or .cabal file:
dependencies:
- notion-client
Usage
Here's a simple example of retrieving a Notion page:
module Main where
import Notion.V1
import Notion.V1.Pages
import Data.Text qualified as Text
import System.Environment qualified as Environment
main :: IO ()
main = do
token <- Environment.getEnv "NOTION_TOKEN"
clientEnv <- getClientEnv "https://api.notion.com/v1"
let Methods{ retrievePage } = makeMethods clientEnv (Text.pack token)
page <- retrievePage "page-id-here"
print page
Creating a page with typed properties
import Notion.V1
import Notion.V1.Common (Parent(..))
import Notion.V1.Pages
import Notion.V1.PropertyValue qualified as PV
import Data.Map qualified as Map
import Data.Vector qualified as Vector
createNewPage :: Methods -> IO PageObject
createNewPage Methods{createPage} = do
let pageProperties = Map.fromList
[ ("title", PV.titleValue (Vector.singleton titleRichText))
, ("Status", PV.selectValue "In Progress")
, ("Priority", PV.selectValue "High")
, ("Due", PV.dateValue "2024-06-01" Nothing)
, ("Score", PV.numberValue 42)
, ("Done", PV.checkboxValue False)
]
newPage = mkCreatePage
(DataSourceParent { dataSourceId = "data-source-id-here" })
pageProperties
createPage newPage
Reading typed properties
import Notion.V1.PropertyValue
readPageStatus :: PageObject -> Maybe Text
readPageStatus page =
case Map.lookup "Status" (properties page) of
Just (SelectValue _ (Just opt)) -> Just (name opt)
_ -> Nothing
Creating a page with markdown
import Notion.V1
import Notion.V1.Common (Parent(..))
import Notion.V1.Pages
createMarkdownPage :: Methods -> IO PageObject
createMarkdownPage Methods{createPage} = do
let newPage = (mkCreatePage
(DataSourceParent { dataSourceId = "data-source-id" })
mempty)
{ markdown = Just "# Hello\n\nThis page was created with **markdown**." }
createPage newPage
Editing page content with markdown
import Notion.V1
import Notion.V1.Pages
editPage :: Methods -> PageID -> IO PageMarkdown
editPage Methods{updatePageMarkdown} pageId =
updatePageMarkdown pageId $
UpdateContent UpdateContentRequest
{ contentUpdates = fromList
[ ContentUpdate
{ oldStr = "old text"
, newStr = "new text"
, replaceAllMatches = Nothing
}
]
, allowDeletingContent = Nothing
}
Error handling
import Control.Exception (catch)
import Notion.V1.Error (NotionError(..))
safeRetrieve :: Methods -> PageID -> IO ()
safeRetrieve Methods{retrievePage} pageId =
retrievePage pageId `catch` \(e :: NotionError) ->
putStrLn $ "Notion error: " <> code e <> " - " <> message e
import Notion.V1.Pagination (paginateAll)
import Notion.V1.DataSources (QueryDataSource(..))
allPages <- paginateAll $ \cursor ->
queryDataSource methods dsId QueryDataSource
{ filter = Nothing, sorts = Nothing
, startCursor = cursor, pageSize = Just 100
, inTrash = Nothing, filterProperties = Nothing
}
Usage with effectful
Callers that use the effectful
effect system can opt into an Eff-typed surface via the companion
package
notion-client-effectful. Every
Notion.V1.Methods field is re-exposed as a smart constructor of a
Notion effect, and a runNotion interpreter dispatches through a
concrete Methods value. NotionError responses surface via the
Error NotionError effect rather than as IO exceptions.
module Demo where
import Data.Text (Text)
import Data.Text qualified as Text
import Effectful (Eff, runEff, (:>))
import Effectful.Error.Static (Error, runErrorNoCallStack)
import Notion.V1 (getClientEnv, makeMethods)
import Notion.V1.Common (UUID (..))
import Notion.V1.Effectful qualified as NE
import Notion.V1.Error (NotionError)
import System.Environment qualified as Env
demo :: (NE.Notion :> es, Error NotionError :> es) => UUID -> Eff es Text
demo pid = do
page <- NE.retrievePage pid
pure (Text.pack (show page))
main :: IO ()
main = do
token <- Text.pack <$> Env.getEnv "NOTION_TOKEN"
env <- getClientEnv "https://api.notion.com/v1"
let methods = makeMethods env token
result <-
runEff . runErrorNoCallStack @NotionError . NE.runNotion methods $
demo (UUID "00000000-0000-0000-0000-000000000000")
print result
See notion-client-effectful/README.md
for the full import pattern (qualifying one of the two Notion.V1
modules is required — every smart constructor shares its name with
the matching Methods record selector).
API Coverage
- Databases: Create, retrieve, update, and query databases
- Data Sources: Create, retrieve, update, query data sources; list templates
- Pages: Create (with blocks or markdown), retrieve, update, move pages; retrieve and update page markdown
- Blocks: Retrieve, update, append children (with position control), and delete blocks
- Users: List, retrieve users and bot users
- Views: Create, retrieve, update, delete, list, and query database views (all 10 view types)
- Search: Search for pages and data sources
- Comments: Create and list comments
- Custom Emojis: List workspace custom emojis
- Webhooks: Event types (including view events) and signature verification
Running the Example
The repository includes a comprehensive example in the notion-client-example directory that demonstrates how to use most API endpoints.
To run the example:
# Set required environment variables
export NOTION_TOKEN="your-integration-token"
# Optional: Set these if you want to test specific database/page endpoints
export NOTION_TEST_DATABASE_ID="your-database-id"
export NOTION_TEST_PAGE_ID="your-page-id"
# Run the example
cabal run notion-client-example
Obtaining API Credentials
- Create a Notion integration at https://www.notion.so/my-integrations
- Get your integration token from the integration settings
- Share any Notion pages or databases you want to access with your integration
- Open the page/database in Notion
- Click "Share" in the top right
- Enter your integration name and click "Invite"
- Get the page/database IDs from their URLs:
- Page URL:
https://www.notion.so/Your-Page-Title-83715d7c1111424aaa11d7fc1111bd2a
- Page ID:
83715d7c1111424aaa11d7fc1111bd2a (the last part of the URL)
License
MIT