Notion Connector Example
An OAuth connector for Notion demonstrating custom provider configuration.
Overview
- Type: Connector
- OAuth Provider: Custom (Notion)
- Capabilities: Search pages, Read pages, List databases
Key Difference: Custom Provider
Unlike Google (built-in), Notion requires custom OAuth URLs:
json
{
"oauth": {
"provider": "custom",
"providerName": "Notion",
"authorizationUrl": "https://api.notion.com/v1/oauth/authorize",
"tokenUrl": "https://api.notion.com/v1/oauth/token",
"scopes": [],
"clientIdSecret": "NOTION_CLIENT_ID",
"clientSecretSecret": "NOTION_CLIENT_SECRET"
}
}Full Code
js
/**
* Notion Connector - Search and read Notion pages
*
* Demonstrates:
* - Custom OAuth provider configuration
* - Notion API v2022-06-28
* - Handling Notion's unique data structures
*/
const NOTION_API = 'https://api.notion.com/v1';
const NOTION_VERSION = '2022-06-28';
async function handler(input, context) {
const { action, ...params } = input;
// Check OAuth connection
if (!await context.oauth.isConnected()) {
return {
error: true,
message: 'Notion not connected. Please connect your Notion account.',
requiresAuth: true
};
}
try {
switch (action) {
case 'search':
return await search(params, context);
case 'getPage':
return await getPage(params, context);
case 'listDatabases':
return await listDatabases(context);
case 'queryDatabase':
return await queryDatabase(params, context);
default:
return {
error: true,
message: `Unknown action: ${action}. Valid: search, getPage, listDatabases, queryDatabase`
};
}
} catch (error) {
return { error: true, message: error.message || 'Notion operation failed' };
}
}
// Helper: Make Notion API request with required headers
async function notionFetch(context, endpoint, options = {}) {
const url = endpoint.startsWith('http') ? endpoint : `${NOTION_API}${endpoint}`;
return context.oauth.fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
'Notion-Version': NOTION_VERSION,
...options.headers,
},
});
}
// Helper: Extract title from Notion page/database
function extractTitle(obj) {
if (obj.properties?.title?.title?.[0]?.plain_text) {
return obj.properties.title.title[0].plain_text;
}
if (obj.properties?.Name?.title?.[0]?.plain_text) {
return obj.properties.Name.title[0].plain_text;
}
if (obj.title?.[0]?.plain_text) {
return obj.title[0].plain_text;
}
return 'Untitled';
}
async function search({ query, filter }, context) {
if (!query) {
return { error: true, message: 'Search query is required' };
}
const body = {
query,
page_size: 20,
};
// Optional: filter by type
if (filter === 'page' || filter === 'database') {
body.filter = { property: 'object', value: filter };
}
const resp = await notionFetch(context, '/search', {
method: 'POST',
body: JSON.stringify(body),
});
if (!resp.ok) {
throw new Error(`Notion search failed: ${resp.status}`);
}
const data = JSON.parse(resp.body);
const results = data.results.map(item => ({
id: item.id,
type: item.object,
title: extractTitle(item),
url: item.url,
lastEdited: item.last_edited_time,
}));
return {
success: true,
action: 'search',
query,
count: results.length,
results,
};
}
async function getPage({ pageId }, context) {
if (!pageId) {
return { error: true, message: 'Page ID is required' };
}
// Get page metadata
const pageResp = await notionFetch(context, `/pages/${pageId}`);
if (!pageResp.ok) {
if (pageResp.status === 404) {
return { error: true, message: 'Page not found or not shared with integration' };
}
throw new Error(`Failed to get page: ${pageResp.status}`);
}
const page = JSON.parse(pageResp.body);
// Get page content (blocks)
const blocksResp = await notionFetch(context, `/blocks/${pageId}/children`);
let content = [];
if (blocksResp.ok) {
const blocksData = JSON.parse(blocksResp.body);
content = blocksData.results.map(block => {
const type = block.type;
const data = block[type];
// Extract text from rich_text arrays
if (data?.rich_text) {
return {
type,
text: data.rich_text.map(t => t.plain_text).join(''),
};
}
return { type, data };
});
}
return {
success: true,
action: 'getPage',
page: {
id: page.id,
title: extractTitle(page),
url: page.url,
created: page.created_time,
lastEdited: page.last_edited_time,
properties: page.properties,
},
content,
};
}
async function listDatabases(context) {
const resp = await notionFetch(context, '/search', {
method: 'POST',
body: JSON.stringify({
filter: { property: 'object', value: 'database' },
page_size: 50,
}),
});
if (!resp.ok) {
throw new Error(`Failed to list databases: ${resp.status}`);
}
const data = JSON.parse(resp.body);
const databases = data.results.map(db => ({
id: db.id,
title: extractTitle(db),
url: db.url,
description: db.description?.[0]?.plain_text,
}));
return {
success: true,
action: 'listDatabases',
count: databases.length,
databases,
};
}
async function queryDatabase({ databaseId, filter, sorts }, context) {
if (!databaseId) {
return { error: true, message: 'Database ID is required' };
}
const body = { page_size: 50 };
if (filter) {
body.filter = typeof filter === 'string' ? JSON.parse(filter) : filter;
}
if (sorts) {
body.sorts = typeof sorts === 'string' ? JSON.parse(sorts) : sorts;
}
const resp = await notionFetch(context, `/databases/${databaseId}/query`, {
method: 'POST',
body: JSON.stringify(body),
});
if (!resp.ok) {
throw new Error(`Failed to query database: ${resp.status}`);
}
const data = JSON.parse(resp.body);
const items = data.results.map(item => ({
id: item.id,
url: item.url,
properties: item.properties,
}));
return {
success: true,
action: 'queryDatabase',
databaseId,
count: items.length,
items,
};
}
module.exports = { handler };Manifest
json
{
"trigger": "auto",
"oauth": {
"provider": "custom",
"providerName": "Notion",
"authorizationUrl": "https://api.notion.com/v1/oauth/authorize",
"tokenUrl": "https://api.notion.com/v1/oauth/token",
"scopes": [],
"clientIdSecret": "NOTION_CLIENT_ID",
"clientSecretSecret": "NOTION_CLIENT_SECRET"
},
"developerSecrets": {
"NOTION_CLIENT_ID": {
"description": "Notion OAuth Client ID from your integration",
"required": true
},
"NOTION_CLIENT_SECRET": {
"description": "Notion OAuth Client Secret",
"required": true
}
},
"tool": {
"name": "notion",
"description": "Search and read Notion pages and databases. Use when users ask about their Notion workspace.",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["search", "getPage", "listDatabases", "queryDatabase"],
"description": "The Notion action to perform"
},
"query": {
"type": "string",
"description": "For search: text to search for"
},
"filter": {
"type": "string",
"description": "For search: 'page' or 'database'. For queryDatabase: JSON filter object"
},
"pageId": {
"type": "string",
"description": "For getPage: the page ID"
},
"databaseId": {
"type": "string",
"description": "For queryDatabase: the database ID"
}
},
"required": ["action"]
}
}
}Setup
Create Notion Integration
- Go to Notion Integrations
- Click "New Integration"
- Name: "PIE Connector" (or your choice)
- Select workspace
- Enable capabilities: Read content, Read user info
Enable OAuth
- In integration settings, go to "Distribution"
- Enable "Public integration"
- Add redirect URI:
https://your-pie-domain.com/api/oauth/plugin/{pluginId}/callback - Copy Client ID and Secret
Create Agent in PIE
- Type: Connector
- Paste code and manifest
- Add credentials as developer secrets
User Connection
- When users connect, they select which pages to share
- Integration only sees pages explicitly shared
Key Patterns
Custom OAuth Provider
Notion doesn't have built-in support, so we specify URLs:
json
{
"provider": "custom",
"authorizationUrl": "https://api.notion.com/v1/oauth/authorize",
"tokenUrl": "https://api.notion.com/v1/oauth/token"
}Required Headers
Notion requires Notion-Version header:
js
async function notionFetch(context, endpoint, options = {}) {
return context.oauth.fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28',
...options.headers,
},
});
}Permission Model
Unlike Google, Notion uses a page-based permission model:
- Users choose which pages to share during OAuth
- Integrations only see explicitly shared content
- No global access to workspace