Notion Integration Guide
Status: Operational | Last Updated: 2026-02-20
Overview
The Yappa Knowledge Hub uses Notion as a primary knowledge management platform, providing a structured database system for organizing knowledge items, categories, and digest reports. The integration enables bidirectional synchronization between the application and Notion, allowing teams to manage knowledge through both the Slack bot interface and Notion's powerful database features.
Key Features
- Bidirectional Sync: Changes in Notion reflect in the app and vice versa
- Three-Database Architecture: Knowledge Items, Categories, and Digest Reports
- Rich Property Mapping: Full support for Notion's property types
- Automatic Pagination: Handles large datasets efficiently
- Rate Limiting: Built-in retry logic with exponential backoff
- Error Recovery: Comprehensive error handling and logging
Database Architecture
1. Knowledge Items Database
Purpose: Central repository for all knowledge entries captured from Slack or created directly in Notion.
Database ID: 306e292a-15d5-8004-a8cb-c222dcd48bb2
URL: https://notion.so/306e292a15d58004a8cbc222dcd48bb2
Schema:
| Property | Type | Required | Description |
|---|---|---|---|
| Title | Title | Yes | Knowledge item title |
| Content | Rich Text | Yes | Main content (max 2000 chars) |
| Tags | Multi-Select | No | Categorization tags (150+ options) |
| Status | Status | No | Not started, In progress, Done |
| Categories | Relation | No | Links to Categories database |
| Source URL | URL | No | Original source link |
| Author | People | No | Content author |
| Attachments | Files | No | Attached files |
| Slack ID | Rich Text | No | Slack user ID reference |
| Channel | Rich Text | No | Slack channel reference |
| TS | Rich Text | No | Slack message timestamp |
| AI Summary | Rich Text | No | AI-generated summary |
Relations:
CategoriesLinks to Categories database (many-to-many)
2. Categories Database
Purpose: Organizational structure for grouping knowledge items by topic, team, or purpose.
Database ID: 306e292a-15d5-805d-ae13-e64bed8519c5
URL: https://notion.so/306e292a15d5805dae13e64bed8519c5
Schema:
| Property | Type | Required | Description |
|---|---|---|---|
| Title | Title | Yes | Category name |
| Description | Rich Text | No | Category description |
| Default Target Groups | Multi-Select | No | Developers, DevOps, Product Managers, etc. |
| Knowledge Items | Relation | No | Links to Knowledge Items database |
| Digest Reports | Relation | No | Links to Digest Reports database |
| Subscribers | People | No | People subscribed to category |
| Rollup | Rollup | No | Count of Knowledge Items |
| Created | Created Time | No | Auto-populated |
| Icon | Page Icon | No | Emoji icon (set via API) |
Relations:
Knowledge ItemsLinks to Knowledge Items database (many-to-many)Digest ReportsLinks to Digest Reports database (many-to-many)
Note: Category icons are set via the page icon API, not as a database property.
3. Digest Reports Database
Purpose: Curated weekly/periodic reports summarizing knowledge items for distribution.
Database ID: 306e292a-15d5-80d7-a0f6-fe8421baff10
URL: https://notion.so/306e292a15d580d7a0f6fe8421baff10
Schema:
| Property | Type | Required | Description |
|---|---|---|---|
| Title | Title | Yes | Report title |
| Period Start | Date | Yes | Digest period start |
| Period End | Date | Yes | Digest period end |
| Status | Status | No | Not started, In progress, Done |
| Items Count | Number | No | Total items in digest |
| Categories | Relation | No | Links to Categories database |
| Generated By | People | No | Report generator |
| Generated At | Created Time | No | Auto-populated |
| Recipients | Rich Text | No | Report recipients |
| Slack Sent | Checkbox | No | Whether sent via Slack |
| Target Groups | Multi-Select | No | Target audience |
Relations:
CategoriesLinks to Categories database (many-to-many)
Sync Architecture
Bidirectional Synchronization
The application maintains data in both SQLite (local) and Notion (cloud), with the following sync patterns:
Create Flow
User Action (Slack) Backend API SQLite (create) Notion (create) Link records- User creates knowledge via Slack bot
- Backend creates record in SQLite database
- Backend creates corresponding page in Notion
- SQLite record updated with
notionIdandnotionUrl
Read Flow
API Request Notion Query Transform Return to Client- API queries Notion database directly
- Results transformed using
NotionPropertyMapper - Data returned to Slack bot or frontend
Update Flow
Update Request SQLite (update) Notion (update) Confirm- Update applied to SQLite record
- Corresponding Notion page updated via API
- Changes synchronized bidirectionally
Search Flow
Search Query SQLite Search + Notion Search Merge Results Return- Query executed against both SQLite and Notion
- Results merged with Notion results prioritized
- Deduplicated results returned
Sync Service Architecture
The NotionSyncService handles all synchronization operations:
File: https://github.com/undead2146/KnowledgeHub/blob/main/backend/src/Service/Notion/NotionSyncService.php
Key Methods:
createKnowledge()- Creates in both SQLite and NotionsearchKnowledge()- Searches both sources and merges resultsgetKnowledge()- Retrieves from SQLite or falls back to NotionsyncToNotion()- Batch syncs existing SQLite records to Notion
Sync Status Tracking:
- SQLite entities have
notionIdandnotionUrlfields - Records without
notionIdare considered unsynced - Batch sync processes unsynced records in configurable batches
Property Mappings
PHP to Notion Mapping
The NotionPropertyMapper class handles all property transformations:
File: https://github.com/undead2146/KnowledgeHub/blob/main/backend/src/Service/Notion/NotionPropertyMapper.php
| PHP Type | Notion Type | Mapper Method | Example |
|---|---|---|---|
| string | title | title($text) | ['title' => [['text' => ['content' => 'Title']]]] |
| string | rich_text | richText($text) | ['rich_text' => [['text' => ['content' => 'Text']]]] |
| array | multi_select | multiSelect($values) | ['multi_select' => [['name' => 'tag1'], ['name' => 'tag2']]] |
| string | select | select($value) | ['select' => ['name' => 'Option']] |
| string | status | status($status) | ['status' => ['name' => 'In progress']] |
| array | relation | relation($pageIds) | ['relation' => [['id' => 'uuid1'], ['id' => 'uuid2']]] |
| string | url | url($url) | ['url' => 'https://example.com'] |
| string | date | date($start, $end) | ['date' => ['start' => '2026-02-20', 'end' => null]] |
| int | number | number($value) | ['number' => 42] |
| bool | checkbox | checkbox($value) | ['checkbox' => true] |
Notion to PHP Extraction
| Notion Type | Extractor Method | Return Type |
|---|---|---|
| title | extractTitle($property) | string |
| rich_text | extractRichText($property) | string |
| multi_select | extractMultiSelect($property) | array |
| select | extractSelect($property) | string|null |
| status | extractStatus($property) | string|null |
| relation | extractRelation($property) | array |
| url | extractUrl($property) | string|null |
| date | extractDate($property) | array|null |
| number | extractNumber($property) | int|null |
| checkbox | extractCheckbox($property) | bool |
Special Handling
Rich Text Truncation:
// Notion has a 2000 character limit for rich text
NotionPropertyMapper::richText(substr($text, 0, 2000))Relation Validation:
// Only add relations if valid Notion UUID format
if ($this->isValidNotionId($categoryId)) {
$properties['Categories'] = NotionPropertyMapper::relation([$categoryId]);
}Icon Handling:
// Icons are set via page icon API, not as properties
$icon = ['type' => 'emoji', 'emoji' => ''];
$this->client->createPageWithIcon($databaseId, $properties, $icon);Rate Limiting and Error Handling
Rate Limits
Notion API Limits:
- Free tier: 3 requests per second
- Team plan: Higher limits (varies)
- Burst limit: Short bursts allowed, then throttled
Retry Strategy
The NotionClient implements exponential backoff for rate limit errors:
Configuration:
private const MAX_RETRIES = 3;
private const RETRY_DELAY_MS = 1000;Retry Logic:
- 429 (Rate Limited): Exponential backoff (1s, 2s, 4s)
- 5xx (Server Error): Fixed 1s delay between retries
- 4xx (Client Error): No retry, throw exception immediately
Example:
// Attempt 1: Immediate
// Attempt 2: Wait 1000ms (1s)
// Attempt 3: Wait 2000ms (2s)
// Attempt 4: Wait 4000ms (4s)
$delay = RETRY_DELAY_MS * pow(2, $attempt - 1);Error Handling
Exception Types:
NotionException- Custom exception for Notion-specific errorsRuntimeException- Generic runtime errorsGuzzleException- HTTP client errors
Error Logging:
$this->logger->warning("Notion API error", [
'method' => $method,
'endpoint' => $endpoint,
'attempt' => $attempt,
'status' => $statusCode,
]);Common Error Codes:
400- Invalid request (bad property format)401- Unauthorized (invalid API key)403- Forbidden (no access to resource)404- Not found (database/page doesn't exist)429- Rate limited (too many requests)500- Server error (Notion internal error)503- Service unavailable (Notion maintenance)
Pagination Handling
Automatic Pagination:
public function queryDatabaseWithPagination(
string $databaseId,
array $filter = [],
array $sorts = [],
int $pageSize = 100
): array {
$allResults = [];
$hasMore = true;
$startCursor = null;
while ($hasMore) {
$payload = ['page_size' => $pageSize];
if ($startCursor !== null) {
$payload['start_cursor'] = $startCursor;
}
$response = $this->request('POST', "databases/{$databaseId}/query", $payload);
$allResults = array_merge($allResults, $response['results'] ?? []);
$hasMore = $response['has_more'] ?? false;
$startCursor = $response['next_cursor'] ?? null;
}
return ['results' => $allResults, 'has_more' => false, 'next_cursor' => null];
}Benefits:
- Automatically fetches all pages
- No manual cursor management required
- Handles large datasets (1000+ items)
- Logs progress for monitoring
Troubleshooting
Common Issues
1. "unauthorized" or "invalid_api_key"
Symptoms: All API requests fail with 401 error
Causes:
- API key is invalid or expired
- API key not properly set in environment variables
- Integration doesn't have access to workspace
Solutions:
# Verify API key in .env
cat backend/.env | grep NOTION_API_KEY
# Regenerate integration token
# 1. Go to https://www.notion.so/my-integrations
# 2. Select your integration
# 3. Click "Show" under Internal Integration Token
# 4. Copy and update .env file
# Restart backend server
cd backend
php -S localhost:8000 -t public2. "object_not_found" for database
Symptoms: Database queries fail with 404 error
Causes:
- Database ID is incorrect
- Integration doesn't have access to database
- Database was deleted or moved
Solutions:
# Verify database ID
cat backend/.env | grep NOTION_DATABASE
# Share database with integration
# 1. Open database in Notion
# 2. Click "..." menu (top right)
# 3. Click "Add connections"
# 4. Select your integration
# 5. Click "Confirm"
# Test database access
curl -X POST "https://api.notion.com/v1/databases/YOUR_DB_ID/query" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Notion-Version: 2022-06-28" \
-H "Content-Type: application/json" \
-d '{"page_size": 1}'3. "validation_error" on page creation
Symptoms: Creating pages fails with 400 error
Causes:
- Property names don't match database schema
- Property types are incorrect
- Multi-select values don't exist in database
- Relation IDs are invalid
Solutions:
// Verify property names (case-sensitive)
// Correct: 'Title', 'Content', 'Tags'
// Wrong: 'title', 'content', 'tags'
// Check property types match
$properties = [
'Title' => NotionPropertyMapper::title($data['title']), // Not richText!
'Content' => NotionPropertyMapper::richText($data['content']),
'Tags' => NotionPropertyMapper::multiSelect($data['tags']), // Not select!
];
// Validate relation IDs
if ($this->isValidNotionId($categoryId)) {
$properties['Categories'] = NotionPropertyMapper::relation([$categoryId]);
}4. Empty query results
Symptoms: Queries return no results despite database having content
Causes:
- Integration doesn't have read permissions
- Filter is too restrictive
- Database is actually empty
Solutions:
# Verify database has content
# Open database in Notion and check for pages
# Test without filters
curl -X POST "https://api.notion.com/v1/databases/YOUR_DB_ID/query" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Notion-Version: 2022-06-28" \
-H "Content-Type: application/json" \
-d '{"page_size": 10}'
# Check integration permissions
# 1. Open database in Notion
# 2. Click "..." menu
# 3. Click "Connections"
# 4. Verify your integration is listed5. Relation property not working
Symptoms: Relations don't appear in Notion or fail to create
Causes:
- Relation not configured in database schema
- Invalid page ID format
- Related page doesn't exist
- Integration doesn't have access to related database
Solutions:
// Validate UUID format
private function isValidNotionId(string $id): bool {
return preg_match(
'/^[a-f0-9]{8}-?[a-f0-9]{4}-?[a-f0-9]{4}-?[a-f0-9]{4}-?[a-f0-9]{12}$/i',
$id
) === 1;
}
// Check relation exists in schema
// 1. Open database in Notion
// 2. Check property list for relation property
// 3. Verify it points to correct database
// Ensure both databases are shared with integration
// Relations require access to both databases6. Rate limiting (429 errors)
Symptoms: Requests fail with "rate_limited" error
Causes:
- Too many requests in short time
- Batch operations without delays
- Multiple concurrent processes
Solutions:
// Use built-in retry logic (automatic)
// NotionClient handles 429 with exponential backoff
// Add delays in batch operations
foreach ($items as $item) {
$this->notionClient->createPage($databaseId, $properties);
usleep(350000); // 350ms delay = ~3 requests/second
}
// Use pagination instead of multiple queries
$results = $this->client->queryDatabaseWithPagination($databaseId);Debugging Tips
Enable Debug Logging:
// Check backend logs
tail -f backend/var/log/dev.log
// Look for Notion API requests
grep "Notion API" backend/var/log/dev.logTest API Directly:
# Get database schema
curl "https://api.notion.com/v1/databases/YOUR_DB_ID" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Notion-Version: 2022-06-28"
# Query database
curl -X POST "https://api.notion.com/v1/databases/YOUR_DB_ID/query" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Notion-Version: 2022-06-28" \
-H "Content-Type: application/json" \
-d '{"page_size": 10}'
# Get specific page
curl "https://api.notion.com/v1/pages/PAGE_ID" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Notion-Version: 2022-06-28"Verify Environment:
# Check all Notion environment variables
cd backend
php -r "
echo 'API Key: ' . (getenv('NOTION_API_KEY') ? 'Set' : 'Missing') . PHP_EOL;
echo 'Version: ' . getenv('NOTION_VERSION') . PHP_EOL;
echo 'Knowledge DB: ' . getenv('NOTION_DATABASE_KNOWLEDGE') . PHP_EOL;
echo 'Categories DB: ' . getenv('NOTION_DATABASE_CATEGORIES') . PHP_EOL;
echo 'Digests DB: ' . getenv('NOTION_DATABASE_DIGESTS') . PHP_EOL;
"Best Practices
1. Property Naming
- Use exact property names from Notion (case-sensitive)
- Verify property names before deployment
- Document custom properties in code comments
2. Data Validation
- Always validate relation IDs before using
- Check multi-select values exist in database
- Truncate rich text to 2000 characters
- Validate URLs before setting URL properties
3. Error Handling
- Always wrap Notion API calls in try-catch blocks
- Log errors with context (database ID, page ID, operation)
- Provide fallback behavior for non-critical operations
- Don't retry on 4xx errors (except 429)
4. Performance
- Use pagination for large datasets
- Batch operations with appropriate delays
- Cache frequently accessed data
- Use filters to reduce result sets
5. Sync Strategy
- Sync immediately for user-initiated actions
- Use batch sync for background operations
- Track sync status with
notionIdfield - Handle partial failures gracefully
Related Documentation
- Notion Setup Guide - Step-by-step setup instructions
- Notion API Usage - API patterns and code examples
- Backend Architecture - Overall system architecture
- Slack Bot Architecture - Slack integration details