Integrations
Notion
Read and write Notion pages, databases, and blocks through Conductor. Covers internal integrations (the quick path), OAuth for multi-workspace apps, the critical page-sharing step that trips up almost everyone, and all database property types.
Authentication
Internal Integration
The simplest option. Creates a bot that lives entirely within one workspace. You get a secret token (starts with secret_) and manually share pages with it. Best for personal use, team automation, and single-workspace tools.
OAuth (Public Integration)
Required if you're building a product that other Notion users will connect to. Users authorize via a Notion consent screen. You receive a per-workspace access token after the OAuth exchange. More complex to set up, but scales to multiple workspaces.
Creating an internal integration
- 01Go to notion.so/my-integrations and click New integration.
- 02Give it a name. Associate it with your workspace. Set it to Internal.
- 03Under Capabilities, choose what the integration can do: Read content, Update content, and Insert content. Note: Delete is not available by design — Notion doesn't expose a delete API.
- 04Click Save, then copy the Internal Integration Secret (the secret_... token).
Critical step — most common mistake
Share pages with your integration
Internal integrations cannot see any pages by default. You must explicitly share each page (or a top-level page, which grants access to all children) with your integration.
- 01Open the page in Notion you want Conductor to access.
- 02Click Share in the top-right corner.
- 03Click Invite, then search for your integration name.
- 04Click Invite to confirm. The integration will now have access to that page and all its children.
If you get object_not_found when calling the API, this is almost always the cause. The page exists — the integration just can't see it yet.
Finding page and database IDs
// Finding Page IDs from Notion URLs:
// https://www.notion.so/workspace/My-Page-Title-abc123456789def01234abcdef012345
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// That 32-char hex string is the page ID.
// With hyphens: abc12345-6789-def0-1234-abcdef012345
// Finding Database IDs:
// Open the database as a full page, then copy the URL.
// Same pattern — 32-char hex string at the end.
// From an inline database, click "..." → "Open as full page" first.Conductor config
Internal integration (most common)
{
"plugins": {
"notion": {
"token": "secret_your-internal-integration-token"
}
}
}OAuth (public integration)
{
"plugins": {
"notion": {
"client_id": "your-oauth-client-id",
"client_secret": "your-oauth-client-secret",
"redirect_uri": "https://yourapp.com/notion/callback",
"access_token": "access_token_from_exchange"
}
}
}Available tools
| Tool | Description |
|---|---|
| notion.page.get | Retrieve a page and its properties by page ID. |
| notion.page.create | Create a new page inside a database or as a child of another page. |
| notion.page.update | Update page properties. Cannot update page content — use block.append for that. |
| notion.database.query | Query a database with optional filters, sorts, and pagination. |
| notion.database.create | Create a new database as a child of a page. |
| notion.block.list | List all child blocks of a page or block. Supports pagination. |
| notion.block.append | Append new blocks to a page or existing block. Supports all block types. |
| notion.search | Search across all pages and databases the integration has access to. |
| notion.user.list | List all users in the workspace (requires user permissions capability). |
Database property types
When creating or updating pages in a database, you must match the property type exactly. The property name is case-sensitive and must match the column name in Notion.
| Type | Format | Notes |
|---|---|---|
| title | { title: [{ text: { content: "..." } }] } | Required for all pages. Every database has one title property. |
| rich_text | { rich_text: [{ text: { content: "..." } }] } | Multi-line text. Supports annotations (bold, italic, etc.). |
| number | { number: 42 } | Numeric value. Can be formatted as currency, percentage, etc. |
| select | { select: { name: "Option" } } | Single choice from a predefined list. Creates new option if not found. |
| multi_select | { multi_select: [{ name: "Tag1" }, { name: "Tag2" }] } | Multiple choices. Order is preserved. |
| date | { date: { start: "2024-06-01", end: "2024-06-30" } } | ISO 8601 date or datetime. end is optional for date ranges. |
| checkbox | { checkbox: true } | Boolean true/false. |
| url | { url: "https://example.com" } | URL string. |
| { email: "user@example.com" } | Email address string. | |
| phone_number | { phone_number: "+1 555-555-5555" } | Phone number string. No validation enforced. |
| relation | { relation: [{ id: "page-id" }] } | References other pages by ID. The target must be accessible to your integration. |
| formula | read-only | Computed value. Cannot be set directly; determined by Notion's formula engine. |
Usage examples
Getting a page
// Get a page by ID
notion.page.get({
page_id: "abc12345-6789-def0-1234-abcdef012345"
})
// The page ID from a Notion URL:
// https://notion.so/My-Page-abc123456789def01234abcdef012345
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Strip hyphens if needed — Conductor accepts both formatsQuerying a database with filters
// Query a database with filters and sorts
notion.database.query({
database_id: "abc12345-6789-def0-1234-abcdef012345",
filter: {
and: [
{
property: "Status",
select: { equals: "In Progress" }
},
{
property: "Due Date",
date: { on_or_before: "2024-12-31" }
}
]
},
sorts: [
{ property: "Due Date", direction: "ascending" }
],
page_size: 25
})Creating a page in a database
// Create a page in a database
notion.page.create({
parent: { database_id: "abc12345-6789-def0-1234-abcdef012345" },
properties: {
"Name": {
title: [{ text: { content: "New task" } }]
},
"Status": {
select: { name: "To Do" }
},
"Due Date": {
date: { start: "2024-06-15" }
},
"Priority": {
select: { name: "High" }
}
}
})
// Create a subpage under another page
notion.page.create({
parent: { page_id: "parent-page-id-here" },
properties: {
title: [{ text: { content: "My Subpage" } }]
}
})Appending blocks to a page
// Append blocks to a page
notion.block.append({
block_id: "abc12345-6789-def0-1234-abcdef012345",
children: [
{
object: "block",
type: "heading_2",
heading_2: {
rich_text: [{ type: "text", text: { content: "Section Title" } }]
}
},
{
object: "block",
type: "paragraph",
paragraph: {
rich_text: [
{ type: "text", text: { content: "Normal text " } },
{
type: "text",
text: { content: "bold text" },
annotations: { bold: true }
}
]
}
},
{
object: "block",
type: "bulleted_list_item",
bulleted_list_item: {
rich_text: [{ type: "text", text: { content: "A list item" } }]
}
}
]
})Searching across all accessible content
// Search across all pages and databases accessible to your integration
notion.search({
query: "project roadmap",
filter: { value: "page", property: "object" }, // "page" or "database"
sort: { direction: "descending", timestamp: "last_edited_time" },
page_size: 10
})OAuth for multi-workspace use
If you're building a product that other Notion users connect, use the public OAuth flow. Create a public integration at notion.so/my-integrations, set a redirect URI, and implement the code exchange.
// After user completes OAuth consent, exchange the code for a token
POST https://api.notion.com/v1/oauth/token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/json
{
"grant_type": "authorization_code",
"code": "code_from_redirect",
"redirect_uri": "https://yourapp.com/notion/callback"
}
// Response includes:
// access_token — store this in Conductor config
// workspace_id — the workspace the user authorized
// workspace_name — human-readable name
// bot_id — the bot ID for your integration in this workspaceThe authorization URL follows the pattern: https://api.notion.com/v1/oauth/authorize?client_id=...&response_type=code&owner=user&redirect_uri=...
Rich text format
Notion represents formatted text as an array of rich text objects. Each object has a text field (the content) and an annotations field (the formatting). Supported annotations: bold, italic, strikethrough, underline, code, color. Links are set via text.link.url.
Notion AI blocks cannot be read or written via the API. If a page contains Notion AI content, that content will be absent from API responses. This is a Notion platform limitation, not a Conductor limitation.
Rate limits
Notion enforces a limit of 3 requests per second per integration. Exceeding this returns HTTP 429 with a Retry-After header. There are no separate tiers — all API methods share the same 3 req/sec budget. For bulk operations (like importing many pages), add a delay between requests or use batching where the API supports it (e.g., block.append accepts up to 100 children in one call).
Common errors
object_not_found (404)
Page or database exists but your integration has not been shared access to it
Fix: Open the page in Notion → click Share → Invite → search for your integration name → Invite.
unauthorized (401)
Token is invalid, expired, or belongs to a different workspace
Fix: Verify the token starts with secret_ for internal integrations. Regenerate if needed.
validation_error (400)
The request body has invalid field values or missing required fields
Fix: Check the property type matches what the database expects. Read the error message detail field.
rate_limited (429)
Exceeded 3 requests/second
Fix: Slow down requests. Implement exponential backoff. Retry after the Retry-After header value.
restricted_resource (403)
Integration lacks capability for this action (e.g., insert but not update)
Fix: Check integration capabilities under your integration settings at notion.so/my-integrations.
Could not find page/database
Passing the page URL or name instead of the UUID
Fix: Use the 32-character UUID from the URL, not the page title or full URL.