Elixir SDK for the Notion API, generated from committed upstream Notion
reference fixtures and executed through the shared pristine runtime.
That pristine dependency is intentional. notion_sdk targets the bounded
public runtime surface:
Pristine.foundation_context/1Pristine.execute_request/3Pristine.SDK.OpenAPI.ClientPristine.stream/3Pristine.OAuth2
It does not treat broad Pristine.Core.* internals as
its SDK contract.
Auth ownership is split intentionally:
Pristine.OAuth2owns the generic OAuth runtime behaviorNotionSDK.OAuthowns Notion-specific helper semantics and CLI UX- durable install and secret authority stay outside the SDK
NotionSDK is intentionally thin:
- generated endpoint modules stay close to upstream JSON payloads
NotionSDK.Clientowns Notion-specific runtime defaults and auth behavior- generic transport, retry, telemetry, and path-safety behavior comes from
pristine - hand-written guides explain the supported runtime contract and common workflows around the generated API reference
The client selects Notion-specific defaults for auth, retry groups, transport
options, and headers.
Generated modules now emit request maps with stable runtime metadata and pass
them through the shared Pristine.execute_request/3 boundary. Workspace
resource ids stay on each request:
That boundary is intentional. notion_sdk keeps its public SDK surface in
NotionSDK.*, while the lower unary HTTP lane stays inside pristine and its
Execution Plane-backed transport substrate instead of becoming a repo-local
public path here.
{:ok, page} =
NotionSDK.Pages.retrieve(client, %{
"page_id" => "00000000-0000-0000-0000-000000000000"
})def deps do
[
{:notion_sdk, "~> 0.2.1"}
]
endThen fetch dependencies:
mix deps.getFor active local development beside sibling checkouts, notion_sdk can also be
consumed from a relative path:
{:notion_sdk, path: "../notion_sdk"}Within this repo, the shared pristine dependencies now resolve by one stable
policy:
- prefer sibling-relative paths when local checkouts exist for normal compile,
test, docs, and
mix deps.get - use the published dependency surface when running
mix hex.buildormix hex.publish - set
NOTION_SDK_HEX_DEPS=1if you wantmix deps.getto ignore sibling../pristinecheckouts and resolve the published dependency surface instead - otherwise use Hex
pristine ~> 0.2.1plus GitHubsubdir:dependencies forpristine_codegenandpristine_provider_testkit
That removes the need for a committed vendored deps/ tree while keeping
local development and downstream dependency behavior aligned.
Create a client with a bearer token:
client = NotionSDK.Client.new(auth: System.fetch_env!("NOTION_TOKEN"))This env-backed constructor is standalone SDK compatibility. In governed
runtime flows, env vars, app config defaults, OAuth saved token files, request
auth overrides, and workspace ids from OAuth responses cannot satisfy
authority. Pass a NotionSDK.GovernedAuthority value instead:
authority =
NotionSDK.GovernedAuthority.new!(
base_url: "https://api.notion.com",
credential_ref: "credential-handle",
credential_lease_ref: "lease-handle",
target_ref: "notion-target",
workspace_ref: "notion-workspace",
headers: %{"X-Governed-Target" => "notion-target"},
credential_headers: %{"Authorization" => "Bearer materialized-token"}
)
client = NotionSDK.Client.new(governed_authority: authority)Fetch the bot user tied to that token:
{:ok, me} = NotionSDK.Users.get_self(client)Search the workspace:
{:ok, result} =
NotionSDK.Search.search(client, %{
"query" => "Roadmap",
"page_size" => 10
})Responses stay as JSON-shaped maps by default. Opt in to typed request/response validation and generated structs only when you want them:
typed_client =
NotionSDK.Client.new(
auth: System.fetch_env!("NOTION_TOKEN"),
typed_responses: true
)- Getting Started: install, defaults, client creation, and first calls
- Client Configuration: client options, Foundation runtime integration, retry tuning, typed responses, and transport overrides
- Versioning: default Notion version, override rules, and how the committed generated surface is versioned
- Capabilities, Permissions, and Sharing: what must be enabled or shared before content, comment, and user calls succeed
- Pages, Blocks, and Search: read-oriented page, block, markdown, and search flows
- Content Creation and Mutation: create, move, update, and append content
- Data Sources and Databases: metadata, queries, templates, and the
2025-09-03split - File Uploads, Comments, and Users: namespace walkthroughs and smaller edge workflows
- File Uploads and Page Attachments: upload-complete-attach workflows for files, covers, icons, and comments
- OAuth and Auth Overrides: authorization URLs, token exchange, saved token files, and request-scoped auth
- Low-Level Requests: the user-facing custom-request escape hatch on
NotionSDK.Client.request/2 - Pagination, Helpers, and Guards: helper surface around paginated responses and Notion ids
- Errors, Retries, and Observability:
%NotionSDK.Error{}, retry groups, request ids, and telemetry - Regeneration and Parity Workflow: snapshot refresh, code generation, and the JS oracle contract
- Live Examples README: the real-service regression-proof suite, fixture requirements, mutation notes, and grouped runner commands
- Cookbook Examples README: task-oriented workflows that layer multiple endpoints into one runnable flow
examples/run_all.sh: runsmoke,content,data,files,mutations,oauth,cookbook,all, oreverything- Generated module docs on HexDocs: the source of truth for exact request/response shapes on each endpoint wrapper
The live examples use NOTION_EXAMPLE_* environment variables for fixture ids
and URLs. The SDK itself does not read those values unless an example passes
them into a request.
For custom requests that are not covered by a generated wrapper yet, use the
simplified raw request shape documented in
Low-Level Requests. That escape hatch still runs
through the shared pristine request pipeline and path-safety checks.
Most public integrations already have a registered HTTPS redirect URI in Notion. That is the easiest onboarding path:
export NOTION_OAUTH_CLIENT_ID="..."
export NOTION_OAUTH_CLIENT_SECRET="..."
export NOTION_OAUTH_REDIRECT_URI="https://your-app.example.com/notion/callback"
mix notion.oauth --save --manual --no-browserThat flow prints the authorization URL, waits for approval in the browser, then
exchanges the temporary code and saves the token JSON to
~/.config/notion_sdk/oauth/notion.json by default.
Saved token persistence and refresh merge behavior now come from the upstream
Pristine.OAuth2.SavedToken workflow, while mix notion.oauth stays the thin
Notion-specific wrapper around env vars, CLI wording, and default paths.
Those env vars and saved files are standalone onboarding and local persistence
only; governed clients reject them and require NotionSDK.GovernedAuthority.
For persisted bearer auth, point the client at the generic file token source:
client =
NotionSDK.Client.new(
oauth2: [
token_source:
{Pristine.Adapters.TokenSource.File,
path: NotionSDK.OAuthTokenFile.default_path()}
]
)Use the full walkthrough in OAuth and Auth Overrides for loopback redirects, programmatic authorization URLs, refresh flows, and explicit Basic auth overrides on OAuth control endpoints.
The public default remains:
- Notion API version header:
2025-09-03 - JS SDK oracle:
@notionhq/client5.12.0 - Bounded parity inventory:
priv/upstream/parity_inventory.json
You can override the version header per client:
client =
NotionSDK.Client.new(
auth: System.fetch_env!("NOTION_TOKEN"),
notion_version: "2025-09-03"
)notion_sdk does not automatically move its default header forward. The
supported default in this repo stays 2025-09-03 until the committed fixtures,
generated code, and tests move together.
The committed generated surface currently includes fields and request shapes such as:
- block append
positionwithafter_block,start, andend - page create
positionwithafter_block,page_start, andpage_end in_trashfields on modern page, block, database, data source, and upload responsesmeeting_notesblock response support in the generated block unions
If you override notion_version, keep that override explicit in code and test
the affected flows in your workspace. Use
Versioning for the current support
contract.
Surface proved in this package today:
- 35 documented endpoint definitions in the committed bounded parity inventory
- request building for OAuth, markdown, multipart uploads, and custom headers
- helper behavior, retry behavior, and error mapping
Supported maintenance commands:
mix notion.generate
mix notion.refresh
mix notion.refresh --snapshots-onlyThe maintainer tasks accept explicit path overrides such as
--reference-root, --notion-docs-root, and --js-sdk-root, so sibling
checkouts are optional rather than required.
Use Regeneration and Parity Workflow for the artifact map, refresh steps, and oracle details.
Recommended verification loop:
mix compile --warnings-as-errors
mix test
mix dialyzer
mix credo --strict
mix docs